Merged in feat(SW-1275)-cancel-booking-my-stay (pull request #1376)
Feat(SW-1275) cancel booking my stay * feat(SW-1276) UI implementation Desktop part 1 for MyStay * feat(SW-1276) UI implementation Desktop part 2 for MyStay * feat(SW-1276) UI implementation Mobile part 1 for MyStay * refactor: move files from MyStay/MyStay to MyStay * feat(SW-1276) Sidepeek implementation * feat(SW-1276): Refactoring * feat(SW-1276) UI implementation Mobile part 2 for MyStay * feat(SW-1276): translations * feat(SW-1276) fixed skeleton * feat(SW-1276): Added missing translations * feat(SW-1276) fixed translations * feat(SW-1275) cancel modal * feat(SW-1275): Mutate cancel booking * feat(SW-1275) added translations * feat(SW-1275) match current cancellationReason * feat(SW-1275) Added modal for manage stay * feat(SW-1275) Added missing icon * feat(SW-1275) New Dont cancel button * feat(SW-1275) Added preperation for Cancellation number * feat(SW-1275): added --modal-box-shadow * feat(SW-1718) Add to calendar * feat(SW-1718) general add to calendar Approved-by: Niclas Edenvin
This commit is contained in:
@@ -130,6 +130,8 @@
|
||||
--default-modal-overlay-z-index: 100;
|
||||
--default-modal-z-index: 101;
|
||||
|
||||
--modal-box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
interpolate-size: allow-keywords;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
|
||||
50
components/HotelReservation/AddToCalendar/index.tsx
Normal file
50
components/HotelReservation/AddToCalendar/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
import { createEvent } from "ics"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { AddToCalendarProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar"
|
||||
|
||||
export default function AddToCalendar({
|
||||
checkInDate,
|
||||
event,
|
||||
hotelName,
|
||||
renderButton,
|
||||
}: AddToCalendarProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
async function downloadBooking() {
|
||||
try {
|
||||
const d = dt(checkInDate).locale(lang).format("YYYY-MM-DD")
|
||||
const filename = `${hotelName.toLowerCase().split(" ").join("_")}-${d}.ics`
|
||||
|
||||
createEvent(event, (error, value) => {
|
||||
if (error) {
|
||||
console.error("ICS Error:", error)
|
||||
toast.error(intl.formatMessage({ id: "Failed to add to calendar" }))
|
||||
return
|
||||
}
|
||||
|
||||
const file = new File([value], filename, { type: "text/calendar" })
|
||||
const url = URL.createObjectURL(file)
|
||||
const anchor = document.createElement("a")
|
||||
anchor.href = url
|
||||
anchor.download = filename
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
document.body.removeChild(anchor)
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Download error:", error)
|
||||
toast.error(intl.formatMessage({ id: "Failed to add to calendar" }))
|
||||
}
|
||||
}
|
||||
|
||||
return renderButton(downloadBooking)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client"
|
||||
import { createEvent } from "ics"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { AddToCalendarProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/addToCalendar"
|
||||
|
||||
export default function AddToCalendar({
|
||||
checkInDate,
|
||||
event,
|
||||
hotelName,
|
||||
}: AddToCalendarProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
async function downloadBooking() {
|
||||
const d = dt(checkInDate).locale(lang).format("YYYY-MM-DD")
|
||||
const filename = `${hotelName.toLowerCase().split(" ").join("_")}-${d}.ics`
|
||||
|
||||
const file: Blob = await new Promise((resolve, reject) => {
|
||||
createEvent(event, (error, value) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
resolve(new File([value], filename, { type: "text/calendar" }))
|
||||
})
|
||||
})
|
||||
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const anchor = document.createElement("a")
|
||||
anchor.href = url
|
||||
anchor.download = filename
|
||||
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
document.body.removeChild(anchor)
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
intent="text"
|
||||
onPress={downloadBooking}
|
||||
size="small"
|
||||
theme="base"
|
||||
variant="icon"
|
||||
wrapping
|
||||
>
|
||||
<CalendarAddIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
export default function AddToCalendarButton({
|
||||
onPress,
|
||||
}: {
|
||||
onPress: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Button intent="text" size="small" theme="base" wrapping onPress={onPress}>
|
||||
<CalendarAddIcon />
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import AddToCalendar from "./Actions/AddToCalendar"
|
||||
import AddToCalendar from "../../AddToCalendar"
|
||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
||||
import DownloadInvoice from "./Actions/DownloadInvoice"
|
||||
import { generateDateTime } from "./Actions/helpers"
|
||||
import ManageBooking from "./Actions/ManageBooking"
|
||||
@@ -80,6 +81,7 @@ export default function Header({
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
|
||||
/>
|
||||
<ManageBooking
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
|
||||
@@ -23,11 +23,18 @@ export default async function BookingConfirmation({
|
||||
confirmationNumber,
|
||||
}: BookingConfirmationProps) {
|
||||
const lang = getLang()
|
||||
const { booking, hotel, room } =
|
||||
await getBookingConfirmation(confirmationNumber)
|
||||
const bookingConfirmation = await getBookingConfirmation(confirmationNumber)
|
||||
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
|
||||
if (!room) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const arrivalDate = new Date(booking.checkInDate)
|
||||
const departureDate = new Date(booking.checkOutDate)
|
||||
|
||||
@@ -62,7 +69,7 @@ export default async function BookingConfirmation({
|
||||
noOfAdults: booking.adults,
|
||||
noOfChildren: booking.childrenAges?.length,
|
||||
ageOfChildren: booking.childrenAges?.join(","),
|
||||
childBedPreference: booking?.extraBedTypes
|
||||
childBedPreference: booking?.childBedPreferences
|
||||
?.flatMap((c) => Array(c.quantity).fill(invertedBedTypeMap[c.bedType]))
|
||||
.join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
.dialog {
|
||||
background-color: var(--Scandic-Brand-Pale-Peach);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "../cancelStay.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
interface CancelStayConfirmationProps {
|
||||
hotel: Hotel
|
||||
booking: BookingConfirmation["booking"]
|
||||
stayDetails: {
|
||||
checkInDate: string
|
||||
checkOutDate: string
|
||||
nightsText: string
|
||||
adultsText: string
|
||||
childrenText: string
|
||||
}
|
||||
}
|
||||
|
||||
export function CancelStayConfirmation({
|
||||
hotel,
|
||||
booking,
|
||||
stayDetails,
|
||||
}: CancelStayConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalText}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.",
|
||||
},
|
||||
{
|
||||
hotel: hotel.name,
|
||||
checkInDate: stayDetails.checkInDate,
|
||||
checkOutDate: stayDetails.checkOutDate,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No charges were made." })}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.priceContainer}>
|
||||
<div className={styles.info}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Cancellation cost" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{stayDetails.nightsText}, {stayDetails.adultsText}
|
||||
{booking.childrenAges?.length > 0
|
||||
? `, ${stayDetails.childrenText}`
|
||||
: ""}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.price}>
|
||||
<Subtitle color="burgundy" type="one">
|
||||
0 {booking.currencyCode}
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "../cancelStay.module.css"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
interface FinalConfirmationProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
stayDetails: {
|
||||
nightsText: string
|
||||
adultsText: string
|
||||
childrenText: string
|
||||
}
|
||||
}
|
||||
|
||||
export function FinalConfirmation({
|
||||
booking,
|
||||
stayDetails,
|
||||
}: FinalConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.modalText}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({
|
||||
id: "Are you sure you want to continue with the cancellation?",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.priceContainer}>
|
||||
<div className={styles.info}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "Cancellation cost" })}
|
||||
</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{stayDetails.nightsText}, {stayDetails.adultsText}
|
||||
{booking.childrenAges?.length > 0
|
||||
? `, ${stayDetails.childrenText}`
|
||||
: ""}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.price}>
|
||||
<Subtitle color="burgundy" type="one">
|
||||
0 {booking.currencyCode}
|
||||
</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.modalText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.priceContainer {
|
||||
display: flex;
|
||||
padding: var(--Spacing-x2);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.info {
|
||||
border-right: 1px solid var(--Base-Border-Subtle);
|
||||
padding-right: var(--Spacing-x2);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.price {
|
||||
padding-left: var(--Spacing-x2);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import type { CancelStayProps } from ".."
|
||||
|
||||
export default function useCancelStay({
|
||||
booking,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
handleBackToManageStay,
|
||||
}: Omit<CancelStayProps, "hotel">) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const cancelStay = trpc.booking.cancel.useMutation({
|
||||
onMutate: () => setIsLoading(true),
|
||||
onSuccess: (result) => {
|
||||
if (!result) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setBookingStatus()
|
||||
toast.success(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out",
|
||||
},
|
||||
{ currency: booking.currencyCode }
|
||||
)
|
||||
)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
},
|
||||
onSettled: () => {
|
||||
handleCloseModal()
|
||||
},
|
||||
})
|
||||
|
||||
function handleCancelStay() {
|
||||
if (!booking.confirmationNumber) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong. Please try again later.",
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
cancelStay.mutate({
|
||||
confirmationNumber: booking.confirmationNumber,
|
||||
language: lang,
|
||||
})
|
||||
}
|
||||
|
||||
function handleCloseCancelStay() {
|
||||
setCurrentStep(1)
|
||||
setIsLoading(false)
|
||||
handleBackToManageStay()
|
||||
}
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
isLoading,
|
||||
handleCancelStay,
|
||||
handleCloseCancelStay,
|
||||
handleBack: () => setCurrentStep(1),
|
||||
handleForward: () => setCurrentStep(2),
|
||||
}
|
||||
}
|
||||
87
components/HotelReservation/MyStay/CancelStay/index.tsx
Normal file
87
components/HotelReservation/MyStay/CancelStay/index.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { ModalContent } from "../ManageStay/ModalContent"
|
||||
import useCancelStay from "./hooks/useCancelStay"
|
||||
import { CancelStayConfirmation } from "./Confirmation"
|
||||
import { FinalConfirmation } from "./FinalConfirmation"
|
||||
import { formatStayDetails } from "./utils"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export interface CancelStayProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
setBookingStatus: () => void
|
||||
handleCloseModal: () => void
|
||||
handleBackToManageStay: () => void
|
||||
}
|
||||
|
||||
export default function CancelStay({
|
||||
booking,
|
||||
hotel,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
handleBackToManageStay,
|
||||
}: CancelStayProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const {
|
||||
currentStep,
|
||||
isLoading,
|
||||
handleCancelStay,
|
||||
handleCloseCancelStay,
|
||||
handleForward,
|
||||
} = useCancelStay({
|
||||
booking,
|
||||
setBookingStatus,
|
||||
handleCloseModal,
|
||||
handleBackToManageStay,
|
||||
})
|
||||
|
||||
const stayDetails = formatStayDetails({ booking, lang, intl })
|
||||
const isFirstStep = currentStep === 1
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalContent
|
||||
title={
|
||||
isFirstStep
|
||||
? intl.formatMessage({ id: "Cancel stay" })
|
||||
: intl.formatMessage({ id: "Confirm cancellation" })
|
||||
}
|
||||
onClose={handleCloseModal}
|
||||
content={
|
||||
isFirstStep ? (
|
||||
<CancelStayConfirmation
|
||||
hotel={hotel}
|
||||
booking={booking}
|
||||
stayDetails={stayDetails}
|
||||
/>
|
||||
) : (
|
||||
<FinalConfirmation booking={booking} stayDetails={stayDetails} />
|
||||
)
|
||||
}
|
||||
primaryAction={{
|
||||
label: isFirstStep
|
||||
? intl.formatMessage({ id: "Cancel stay" })
|
||||
: intl.formatMessage({ id: "Confirm cancellation" }),
|
||||
onClick: isFirstStep ? handleForward : handleCancelStay,
|
||||
intent: isFirstStep ? "secondary" : "primary",
|
||||
isLoading: isLoading,
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: isFirstStep
|
||||
? intl.formatMessage({ id: "Back" })
|
||||
: intl.formatMessage({ id: "Don't cancel" }),
|
||||
onClick: isFirstStep ? handleCloseCancelStay : handleCloseModal,
|
||||
intent: "text",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
44
components/HotelReservation/MyStay/CancelStay/utils.ts
Normal file
44
components/HotelReservation/MyStay/CancelStay/utils.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export function formatStayDetails({
|
||||
booking,
|
||||
lang,
|
||||
intl,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
lang: string
|
||||
intl: IntlShape
|
||||
}) {
|
||||
const checkInDate = dt(booking.checkInDate)
|
||||
.locale(lang)
|
||||
.format("dddd D MMM YYYY")
|
||||
const checkOutDate = dt(booking.checkOutDate)
|
||||
.locale(lang)
|
||||
.format("dddd D MMM YYYY")
|
||||
const diff = dt(checkOutDate).diff(checkInDate, "days")
|
||||
|
||||
const nightsText = intl.formatMessage(
|
||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
const adultsText = intl.formatMessage(
|
||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
||||
{ totalAdults: booking.adults }
|
||||
)
|
||||
const childrenText = intl.formatMessage(
|
||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
||||
{ totalChildren: booking.childrenAges?.length }
|
||||
)
|
||||
|
||||
return {
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
nightsText,
|
||||
adultsText,
|
||||
childrenText,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CalendarAddIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import styles from "../actionPanel.module.css"
|
||||
|
||||
export default function AddToCalendarButton({
|
||||
onPress,
|
||||
}: {
|
||||
onPress: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="text"
|
||||
theme="base"
|
||||
className={styles.button}
|
||||
onPress={onPress}
|
||||
>
|
||||
{intl.formatMessage({ id: "Add to calendar" })}
|
||||
<CalendarAddIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
.actionPanel {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 432px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.actionPanel .menu .button {
|
||||
width: 100%;
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
justify-content: space-between !important;
|
||||
padding: var(--Spacing-x1) 0 !important;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 256px;
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
padding: var(--Spacing-x3);
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.tag {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--Main-Red-60);
|
||||
font-family: var(--typography-Caption-Labels-fontFamily);
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-top: auto;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { customerService } from "@/constants/currentWebHrefs"
|
||||
|
||||
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
|
||||
import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers"
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronRightIcon,
|
||||
CreditCard,
|
||||
CrossCircleOutlineIcon,
|
||||
DownloadIcon,
|
||||
} from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import AddToCalendarButton from "./Actions/AddToCalendarButton"
|
||||
|
||||
import styles from "./actionPanel.module.css"
|
||||
|
||||
import type { EventAttributes } from "ics"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export default function ActionPanel({
|
||||
booking,
|
||||
hotel,
|
||||
showCancelButton,
|
||||
onCancelClick,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
showCancelButton: boolean
|
||||
onCancelClick: () => void
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const event: EventAttributes = {
|
||||
busyStatus: "FREE",
|
||||
categories: ["booking", "hotel", "stay"],
|
||||
created: generateDateTime(booking.createDateTime),
|
||||
description: hotel.hotelContent.texts.descriptions.medium,
|
||||
end: generateDateTime(booking.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(booking.checkInDate),
|
||||
startInputType: "utc",
|
||||
status: "CONFIRMED",
|
||||
title: hotel.name,
|
||||
url: hotel.contactInformation.websiteUrl,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.actionPanel}>
|
||||
<div className={styles.menu}>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Modify dates" })}
|
||||
<CalendarIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Guarantee late arrival" })}
|
||||
<CreditCard width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<AddToCalendar
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
hotelName={hotel.name}
|
||||
renderButton={(onPress) => <AddToCalendarButton onPress={onPress} />}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Download invoice" })}
|
||||
<DownloadIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
{showCancelButton && (
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onCancelClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Cancel stay" })}
|
||||
<CrossCircleOutlineIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<div>
|
||||
<span className={styles.tag}>
|
||||
{intl.formatMessage({ id: "Reference number" })}
|
||||
</span>
|
||||
<Subtitle color="burgundy" textAlign="right">
|
||||
{booking.confirmationNumber}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<div className={styles.hotel}>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.name}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.address.streetAddress}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" textAlign="right">
|
||||
{hotel.address.city}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" asChild>
|
||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
||||
{hotel.contactInformation.phoneNumber}
|
||||
</Link>
|
||||
</Body>
|
||||
</div>
|
||||
<Link
|
||||
href={customerService[lang]}
|
||||
variant="icon"
|
||||
className={styles.link}
|
||||
>
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Customer support" })}
|
||||
</Caption>
|
||||
<ChevronRightIcon width={20} height={20} color="burgundy" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./modalContent.module.css"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
interface ModalContentProps {
|
||||
title: string
|
||||
content: ReactNode
|
||||
primaryAction: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
intent?: "primary" | "secondary" | "text"
|
||||
isLoading?: boolean
|
||||
}
|
||||
secondaryAction: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
intent?: "primary" | "secondary" | "text"
|
||||
}
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function ModalContent({
|
||||
title,
|
||||
content,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
onClose,
|
||||
}: ModalContentProps) {
|
||||
return (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<Subtitle color="uiTextHighContrast">{title}</Subtitle>
|
||||
<button onClick={onClose} type="button" className={styles.close}>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
<div className={styles.content}>{content}</div>
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
theme="base"
|
||||
intent={secondaryAction.intent ?? "text"}
|
||||
color="burgundy"
|
||||
onClick={secondaryAction.onClick}
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
<Button
|
||||
theme="base"
|
||||
intent={primaryAction.intent ?? "secondary"}
|
||||
onClick={primaryAction.onClick}
|
||||
disabled={primaryAction.isLoading}
|
||||
>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.content {
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x3) var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
136
components/HotelReservation/MyStay/ManageStay/index.tsx
Normal file
136
components/HotelReservation/MyStay/ManageStay/index.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
import { motion } from "framer-motion"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import {
|
||||
type AnimationState,
|
||||
AnimationStateEnum,
|
||||
} from "@/components/Modal/modal"
|
||||
import { slideFromTop } from "@/components/Modal/motionVariants"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import CancelStay from "../CancelStay"
|
||||
import ActionPanel from "./ActionPanel"
|
||||
|
||||
import styles from "./modifyModal.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
type ActiveView = "actionPanel" | "cancelStay"
|
||||
|
||||
export default function ManageStay({
|
||||
booking,
|
||||
hotel,
|
||||
setBookingStatus,
|
||||
bookingStatus,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
setBookingStatus: (status: BookingStatusEnum) => void
|
||||
bookingStatus: string | null
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [animation, setAnimation] = useState<AnimationState>(
|
||||
AnimationStateEnum.visible
|
||||
)
|
||||
const [activeView, setActiveView] = useState<ActiveView>("actionPanel")
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const MotionOverlay = motion(ModalOverlay)
|
||||
const MotionModal = motion(Modal)
|
||||
|
||||
const showCancelButton =
|
||||
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof isOpen === "boolean") {
|
||||
setAnimation(
|
||||
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
||||
)
|
||||
}
|
||||
if (isOpen === undefined) {
|
||||
setAnimation(AnimationStateEnum.unmounted)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
function modalStateHandler(newAnimationState: AnimationState) {
|
||||
setAnimation((currentAnimationState) =>
|
||||
newAnimationState === AnimationStateEnum.hidden &&
|
||||
currentAnimationState === AnimationStateEnum.hidden
|
||||
? AnimationStateEnum.unmounted
|
||||
: currentAnimationState
|
||||
)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false)
|
||||
setActiveView("actionPanel")
|
||||
}
|
||||
function handleBack() {
|
||||
setActiveView("actionPanel")
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
switch (activeView) {
|
||||
case "cancelStay":
|
||||
return (
|
||||
<CancelStay
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
setBookingStatus={() =>
|
||||
setBookingStatus(BookingStatusEnum.Cancelled)
|
||||
}
|
||||
handleCloseModal={handleClose}
|
||||
handleBackToManageStay={handleBack}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<ActionPanel
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
onCancelClick={() => setActiveView("cancelStay")}
|
||||
showCancelButton={showCancelButton}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="icon" fullWidth onClick={() => setIsOpen(true)}>
|
||||
{intl.formatMessage({ id: "Manage stay" })}
|
||||
<ChevronDownIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<MotionOverlay
|
||||
isOpen={isOpen}
|
||||
className={styles.overlay}
|
||||
initial={"hidden"}
|
||||
onAnimationComplete={modalStateHandler}
|
||||
onOpenChange={handleClose}
|
||||
isDismissable
|
||||
>
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
initial={"hidden"}
|
||||
animate={animation}
|
||||
variants={slideFromTop}
|
||||
>
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
aria-label={intl.formatMessage({ id: "Dialog" })}
|
||||
>
|
||||
{renderContent()}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
</MotionOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: var(--default-modal-overlay-z-index);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--default-modal-z-index);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* For removing focus outline when modal opens first time */
|
||||
outline: 0 none;
|
||||
|
||||
/* for supporting animations within content */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: var(--Spacing-x2);
|
||||
width: var(--button-dimension);
|
||||
height: var(--button-dimension);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: auto;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
max-width: var(--max-width-page);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { BookingStatusEnum } from "@/constants/booking"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import ManageStay from "../ManageStay"
|
||||
|
||||
import styles from "./referenceCard.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export async function ReferenceCard({
|
||||
export function ReferenceCard({
|
||||
booking,
|
||||
hotel,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
}) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const [bookingStatus, setBookingStatus] = useState(booking.reservationStatus)
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
const isCancelled = bookingStatus === BookingStatusEnum.Cancelled
|
||||
|
||||
const showCancelButton = !isCancelled && booking.isCancelable
|
||||
|
||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
|
||||
|
||||
return (
|
||||
@@ -36,10 +50,15 @@ export async function ReferenceCard({
|
||||
{intl.formatMessage({ id: "Reference" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast" className={styles.titleDesktop}>
|
||||
{intl.formatMessage({ id: "Reference number" })}
|
||||
{isCancelled
|
||||
? intl.formatMessage({ id: "Cancellation number" })
|
||||
: intl.formatMessage({ id: "Reference number" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast">
|
||||
{booking.confirmationNumber}
|
||||
{/* TODO: Implement this: https://scandichotels.atlassian.net/browse/API2-2883 to get correct cancellation number */}
|
||||
{isCancelled
|
||||
? booking.linkedReservations[0]?.cancellationNumber
|
||||
: booking.confirmationNumber}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" className={styles.divider} />
|
||||
@@ -105,8 +124,26 @@ export async function ReferenceCard({
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
</Caption>
|
||||
</div>
|
||||
{!showCancelButton && (
|
||||
<div className={styles.referenceRow}>
|
||||
<IconChip
|
||||
color={"red"}
|
||||
icon={<CrossCircleIcon width={20} height={20} color="red" />}
|
||||
>
|
||||
<Caption color={"red"}>
|
||||
<strong>{intl.formatMessage({ id: "Status" })}:</strong>{" "}
|
||||
{intl.formatMessage({ id: "Cancelled" })}
|
||||
</Caption>
|
||||
</IconChip>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.actionArea}>
|
||||
<Button fullWidth>{intl.formatMessage({ id: "Manage stay" })}</Button>
|
||||
<ManageStay
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
setBookingStatus={setBookingStatus}
|
||||
bookingStatus={bookingStatus}
|
||||
/>
|
||||
<Button fullWidth intent="secondary" asChild>
|
||||
<Link href={directionsUrl} target="_blank">
|
||||
{intl.formatMessage({ id: "Get directions" })}
|
||||
|
||||
@@ -134,7 +134,7 @@ export function Room({ booking, room, hotel, user }: RoomProps) {
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.rateDefinition.title}
|
||||
{booking.rateDefinition.cancellationText}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { homeHrefs } from "@/constants/homeHrefs"
|
||||
import { env } from "@/env/server"
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -21,7 +23,13 @@ import { Room } from "./Room"
|
||||
import styles from "./myStay.module.css"
|
||||
|
||||
export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
const { booking, hotel, room } = await getBookingConfirmation(reservationId)
|
||||
const bookingConfirmation = await getBookingConfirmation(reservationId)
|
||||
if (!bookingConfirmation) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const { booking, hotel, room } = bookingConfirmation
|
||||
|
||||
const userResponse = await getProfileSafely()
|
||||
const user = userResponse && !("error" in userResponse) ? userResponse : null
|
||||
const intl = await getIntl()
|
||||
@@ -38,14 +46,17 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
<main className={styles.main}>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.blurOverlay} />
|
||||
{hotel.gallery?.heroImages[0].imageSizes.large && (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={hotel.gallery.heroImages[0].imageSizes.large}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={
|
||||
hotel.gallery?.heroImages[0]?.imageSizes.large ??
|
||||
room?.images[0]?.imageSizes.large ??
|
||||
""
|
||||
}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerContainer}>
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.content {
|
||||
width: var(--max-width-content);
|
||||
padding-bottom: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
height: calc(100dvh - 20px);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
|
||||
&[data-entering] {
|
||||
|
||||
27
components/Icons/CrossCircleOutline.tsx
Normal file
27
components/Icons/CrossCircleOutline.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function CrossCircleOutlineIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 13.3L14.9 16.2C15.075 16.375 15.2917 16.4625 15.55 16.4625C15.8083 16.4625 16.025 16.375 16.2 16.2C16.375 16.025 16.4625 15.8083 16.4625 15.55C16.4625 15.2917 16.375 15.075 16.2 14.9L13.3 12L16.2 9.1C16.375 8.925 16.4625 8.70833 16.4625 8.45C16.4625 8.19167 16.375 7.975 16.2 7.8C16.025 7.625 15.8083 7.5375 15.55 7.5375C15.2917 7.5375 15.075 7.625 14.9 7.8L12 10.7L9.1 7.8C8.925 7.625 8.70833 7.5375 8.45 7.5375C8.19167 7.5375 7.975 7.625 7.8 7.8C7.625 7.975 7.5375 8.19167 7.5375 8.45C7.5375 8.70833 7.625 8.925 7.8 9.1L10.7 12L7.8 14.9C7.625 15.075 7.5375 15.2917 7.5375 15.55C7.5375 15.8083 7.625 16.025 7.8 16.2C7.975 16.375 8.19167 16.4625 8.45 16.4625C8.70833 16.4625 8.925 16.375 9.1 16.2L12 13.3ZM12 21.75C10.6516 21.75 9.38434 21.4936 8.19838 20.9809C7.01239 20.4682 5.98075 19.7724 5.10345 18.8934C4.22615 18.0145 3.53125 16.9826 3.01875 15.7978C2.50625 14.613 2.25 13.3471 2.25 12C2.25 10.6516 2.50636 9.38434 3.01908 8.19838C3.53179 7.01239 4.22762 5.98075 5.10658 5.10345C5.98553 4.22615 7.01739 3.53125 8.20218 3.01875C9.38698 2.50625 10.6529 2.25 12 2.25C13.3484 2.25 14.6157 2.50636 15.8016 3.01908C16.9876 3.53179 18.0193 4.22762 18.8966 5.10658C19.7739 5.98553 20.4688 7.01739 20.9813 8.20217C21.4938 9.38697 21.75 10.6529 21.75 12C21.75 13.3484 21.4936 14.6157 20.9809 15.8016C20.4682 16.9876 19.7724 18.0193 18.8934 18.8966C18.0145 19.7739 16.9826 20.4688 15.7978 20.9813C14.613 21.4938 13.3471 21.75 12 21.75ZM12 19.875C14.1917 19.875 16.0521 19.1104 17.5813 17.5813C19.1104 16.0521 19.875 14.1917 19.875 12C19.875 9.80833 19.1104 7.94792 17.5813 6.41875C16.0521 4.88958 14.1917 4.125 12 4.125C9.80833 4.125 7.94792 4.88958 6.41875 6.41875C4.88958 7.94792 4.125 9.80833 4.125 12C4.125 14.1917 4.88958 16.0521 6.41875 17.5813C7.94792 19.1104 9.80833 19.875 12 19.875Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
CoolIcon,
|
||||
CroissantCoffeeEggIcon,
|
||||
CrossCircle,
|
||||
CrossCircleOutlineIcon,
|
||||
CulturalIcon,
|
||||
CutleryOneIcon,
|
||||
CutleryTwoIcon,
|
||||
@@ -154,6 +155,8 @@ export function getIconByIconName(
|
||||
return CheckIcon
|
||||
case IconName.CrossCircle:
|
||||
return CrossCircle
|
||||
case IconName.CrossCircleOutline:
|
||||
return CrossCircleOutlineIcon
|
||||
case IconName.CheckCircle:
|
||||
return CheckCircleIcon
|
||||
case IconName.ChevronDown:
|
||||
|
||||
@@ -60,6 +60,7 @@ export { default as CreditCard } from "./CreditCard"
|
||||
export { default as CreditCardAddIcon } from "./CreditCardAdd"
|
||||
export { default as CroissantCoffeeEggIcon } from "./CroissantCoffeeEgg"
|
||||
export { default as CrossCircle } from "./CrossCircle"
|
||||
export { default as CrossCircleOutlineIcon } from "./CrossCircleOutline"
|
||||
export { default as CulturalIcon } from "./Cultural"
|
||||
export { default as CutleryOneIcon } from "./CutleryOne"
|
||||
export { default as CutleryTwoIcon } from "./CutleryTwo"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -39,7 +39,7 @@
|
||||
align-items: flex-start;
|
||||
min-height: var(--button-dimension);
|
||||
position: relative;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) 0;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -21,3 +21,16 @@ export const slideInOut = {
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
}
|
||||
|
||||
export const slideFromTop = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: -32,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
box-shadow: var(--modal-box-shadow);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
|
||||
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)",
|
||||
"<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult": "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/nat per voksen",
|
||||
"<strong>Status</strong> Paid": "<strong>Status</strong> Betalt",
|
||||
"<strong>Status</strong> Unpaid": "<strong>Status</strong> Ikke betalt",
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.",
|
||||
"A photo of the room": "Et foto af værelset",
|
||||
"ACCE": "Tilgængelighed",
|
||||
@@ -47,6 +49,8 @@
|
||||
"Approx {value}.": "Ca. {value}",
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Approx. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Er du sikker på, at du vil annullere dit ophold hos {hotel} fra {checkInDate} til {checkOutDate}? Dette kan ikke gendannes.",
|
||||
"Are you sure you want to continue with the cancellation?": "Er du sikker på, at du vil fortsætte med annullereringen?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"As our Close Friend": "Som vores nære ven",
|
||||
@@ -101,7 +105,11 @@
|
||||
"Cabaret seating": "Cabaret seating",
|
||||
"Campaign": "Kampagne",
|
||||
"Cancel": "Afbestille",
|
||||
"Cancel stay": "Annuller ophold",
|
||||
"Cancellation cost": "Annulleret pris",
|
||||
"Cancellation number": "Annulleringsnummer",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Cancelled": "Annulleret",
|
||||
"Change room": "Skift værelse",
|
||||
"Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Ændringer kan gøres indtil {time} på {date}, under forudsætning af tilgængelighed. Priserne for værelserne kan variere.",
|
||||
"Check for level upgrade": "Check for level upgrade",
|
||||
@@ -138,6 +146,7 @@
|
||||
"Complete booking": "Fuldfør bookingen",
|
||||
"Complete booking & go to payment": "Udfyld booking & gå til betaling",
|
||||
"Complete the booking": "Fuldfør bookingen",
|
||||
"Confirm cancellation": "Bekræft annullerering",
|
||||
"Contact information": "Kontaktoplysninger",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
"Contact us": "Kontakt os",
|
||||
@@ -155,6 +164,7 @@
|
||||
"Current Points: {points, number}": "Current Points: {points, number}",
|
||||
"Current password": "Nuværende kodeord",
|
||||
"Customer service": "Kundeservice",
|
||||
"Customer support": "Kundesupport",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Dag",
|
||||
@@ -175,6 +185,7 @@
|
||||
"Distance to city center": "Afstand til centrum",
|
||||
"Distance to hotel: {distanceInM} m": "Afstand til hotel: {distance} m",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?",
|
||||
"Don't cancel": "Annuller ikke",
|
||||
"Done": "Færdig",
|
||||
"Download invoice": "Download faktura",
|
||||
"Download the Scandic app": "Download Scandic-appen",
|
||||
@@ -200,6 +211,7 @@
|
||||
"Extra bed will be provided additionally": "Der vil blive stillet en ekstra seng til rådighed",
|
||||
"Extras to your booking": "Tillæg til din booking",
|
||||
"FAQ": "Ofte stillede spørgsmål",
|
||||
"Failed to add to calendar": "Fejl ved tilføjelse til kalender",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
@@ -244,6 +256,7 @@
|
||||
"Go to My Benefits": "Gå til 'Mine fordele'",
|
||||
"Go to profile": "Go to profile",
|
||||
"Guarantee booking with credit card": "Garantere booking med kreditkort",
|
||||
"Guarantee late arrival": "Garanter sen ankomst",
|
||||
"Guest information": "Gæsteinformation",
|
||||
"Guests": "Gæster",
|
||||
"Guests & Rooms": "Gæster & værelser",
|
||||
@@ -294,6 +307,7 @@
|
||||
"Join now": "Tilmeld dig nu",
|
||||
"Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.",
|
||||
"Kayaking": "Kajakroning",
|
||||
"Keep stay": "Behold ophold",
|
||||
"King bed": "Kingsize-seng",
|
||||
"Language": "Sprog",
|
||||
"Last name": "Efternavn",
|
||||
@@ -387,6 +401,7 @@
|
||||
"No": "Nej",
|
||||
"No availability": "Ingen tilgængelighed",
|
||||
"No breakfast": "Ingen morgenmad",
|
||||
"No charges were made.": "Ingen gebyrer blev opkrævet.",
|
||||
"No content published": "Intet indhold offentliggjort",
|
||||
"No hotels match your filters": "Ingen rum matchede dine filtre.",
|
||||
"No matching location found": "Der blev ikke fundet nogen matchende placering",
|
||||
@@ -688,6 +703,7 @@
|
||||
"Your points to spend": "Dine brugbare point",
|
||||
"Your room": "Dit værelse",
|
||||
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
||||
"Zip code": "Postnummer",
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Zoom ind",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
|
||||
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)",
|
||||
"<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult": "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/Nacht pro Erwachsenem",
|
||||
"<strong>Status</strong> Paid": "<strong>Status</strong> Bezahlt",
|
||||
"<strong>Status</strong> Unpaid": "<strong>Status</strong> Unbezahlt",
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.",
|
||||
"A photo of the room": "Ein Foto des Zimmers",
|
||||
"ACCE": "Zugänglichkeit",
|
||||
@@ -47,6 +49,8 @@
|
||||
"Approx {value}.": "Ca. {value}",
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Approx. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Sind Sie sicher, dass Sie Ihren Aufenthalt bei {hotel} vom {checkInDate} bis {checkOutDate} stornieren möchten? Dies kann nicht rückgängig gemacht werden.",
|
||||
"Are you sure you want to continue with the cancellation?": "Sind Sie sicher, dass Sie mit der Stornierung fortfahren möchten?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
||||
"Arrival date": "Ankunftsdatum",
|
||||
"As our Close Friend": "Als unser enger Freund",
|
||||
@@ -102,7 +106,11 @@
|
||||
"Cabaret seating": "Cabaret seating",
|
||||
"Campaign": "Kampagne",
|
||||
"Cancel": "Stornieren",
|
||||
"Cancel stay": "Stornieren",
|
||||
"Cancellation cost": "Stornierungskosten",
|
||||
"Cancellation number": "Stornierungsnummer",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Cancelled": "Storniert",
|
||||
"Change room": "Zimmer ändern",
|
||||
"Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Änderungen können bis {time} am {date} vorgenommen werden, vorausgesetzt, dass die Zimmer noch verfügbar sind. Die Zimmerpreise können variieren.",
|
||||
"Check for level upgrade": "Check for level upgrade",
|
||||
@@ -139,6 +147,7 @@
|
||||
"Complete booking": "Buchung abschließen",
|
||||
"Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen",
|
||||
"Complete the booking": "Buchung abschließen",
|
||||
"Confirm cancellation": "Stornierung bestätigen",
|
||||
"Contact information": "Kontaktinformationen",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
"Contact us": "Kontaktieren Sie uns",
|
||||
@@ -156,6 +165,7 @@
|
||||
"Current Points: {points, number}": "Current Points: {points, number}",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"Customer service": "Kundendienst",
|
||||
"Customer support": "Kundesupport",
|
||||
"Date of Birth": "Geburtsdatum",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Tag",
|
||||
@@ -176,6 +186,7 @@
|
||||
"Distance to city center": "Entfernung zum Stadtzentrum",
|
||||
"Distance to hotel: {distanceInM} m": "Entfernung zum Hotel: {distance} m",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?",
|
||||
"Don't cancel": "Stornieren",
|
||||
"Done": "Fertig",
|
||||
"Download invoice": "Rechnung herunterladen",
|
||||
"Download the Scandic app": "Laden Sie die Scandic-App herunter",
|
||||
@@ -201,6 +212,7 @@
|
||||
"Extra bed will be provided additionally": "Ein zusätzliches Bett wird bereitgestellt",
|
||||
"Extras to your booking": "Extras zu Ihrer Buchung",
|
||||
"FAQ": "Häufig gestellte Fragen",
|
||||
"Failed to add to calendar": "Fehler beim Hinzufügen zum Kalender",
|
||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
@@ -245,6 +257,7 @@
|
||||
"Go to My Benefits": "Gehen Sie zu „Meine Vorteile“",
|
||||
"Go to profile": "Go to profile",
|
||||
"Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren",
|
||||
"Guarantee late arrival": "Garantere sen ankomst",
|
||||
"Guest information": "Informationen für Gäste",
|
||||
"Guests": "Gäste",
|
||||
"Guests & Rooms": "Gäste & Zimmer",
|
||||
@@ -295,6 +308,7 @@
|
||||
"Join now": "Mitglied werden",
|
||||
"Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.",
|
||||
"Kayaking": "Kajakfahren",
|
||||
"Keep stay": "Aufenthalt behalten",
|
||||
"King bed": "Kingsize-Bett",
|
||||
"Language": "Sprache",
|
||||
"Last name": "Nachname",
|
||||
@@ -356,6 +370,7 @@
|
||||
"Menu": "Menü",
|
||||
"Menus": "Menüs",
|
||||
"Modify": "Ändern",
|
||||
"Modify dates": "Ändra datum",
|
||||
"Modify guest details": "Gastdetails ändern",
|
||||
"Mon-Fri Always open": "Mo-Fr Immer geöffnet",
|
||||
"Mon-Fri {openingTime}-{closingTime}": "Mo-Fr {openingTime}-{closingTime}",
|
||||
@@ -388,6 +403,7 @@
|
||||
"No": "Nein",
|
||||
"No availability": "Keine Verfügbarkeit",
|
||||
"No breakfast": "Kein Frühstück",
|
||||
"No charges were made.": "Es wurden keine Gebühren erhoben.",
|
||||
"No content published": "Kein Inhalt veröffentlicht",
|
||||
"No hotels match your filters": "Kein Zimmer entspricht Ihren Filtern.",
|
||||
"No matching location found": "Kein passender Standort gefunden",
|
||||
@@ -688,6 +704,7 @@
|
||||
"Your points to spend": "Meine Punkte",
|
||||
"Your room": "Ihr Zimmer",
|
||||
"Your selected bed type will be provided based on availability": "Ihre ausgewählte Bettart wird basierend auf der Verfügbarkeit bereitgestellt",
|
||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Ihr Aufenthalt wurde storniert. Stornierungskosten: 0 {currency}. Es tut uns leid, dass die Pläne nicht funktionierten",
|
||||
"Zip code": "PLZ",
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Vergrößern",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
|
||||
"<b>Total price</b> (incl VAT)": "<b>Total price</b> (incl VAT)",
|
||||
"<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult": "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult",
|
||||
"<strong>Status</strong> Paid": "<strong>Status</strong> Paid",
|
||||
"<strong>Status</strong> Unpaid": "<strong>Status</strong> Unpaid",
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.",
|
||||
"A photo of the room": "A photo of the room",
|
||||
"ACCE": "Accessibility",
|
||||
@@ -47,6 +49,8 @@
|
||||
"Apply": "Apply",
|
||||
"Approx.": "Approx.",
|
||||
"Approx. {value}": "Approx. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.",
|
||||
"Are you sure you want to continue with the cancellation?": "Are you sure you want to continue with the cancellation?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?",
|
||||
"Arrival date": "Arrival date",
|
||||
"As our Close Friend": "As our Close Friend",
|
||||
@@ -103,7 +107,11 @@
|
||||
"Campaign": "Campaign",
|
||||
"Cancel": "Cancel",
|
||||
"Cancel booking": "Cancel booking",
|
||||
"Cancel stay": "Cancel stay",
|
||||
"Cancellation cost": "Cancellation cost",
|
||||
"Cancellation number": "Cancellation number",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Cancelled": "Cancelled",
|
||||
"Change room": "Change room",
|
||||
"Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.",
|
||||
"Check for level upgrade": "Check for level upgrade",
|
||||
@@ -140,6 +148,7 @@
|
||||
"Complete booking": "Complete booking",
|
||||
"Complete booking & go to payment": "Complete booking & go to payment",
|
||||
"Complete the booking": "Complete the booking",
|
||||
"Confirm cancellation": "Confirm cancellation",
|
||||
"Contact information": "Contact information",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
"Contact us": "Contact us",
|
||||
@@ -158,6 +167,7 @@
|
||||
"Current Points: {points, number}": "Current Points: {points, number}",
|
||||
"Current password": "Current password",
|
||||
"Customer service": "Customer service",
|
||||
"Customer support": "Customer support",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Day",
|
||||
@@ -178,6 +188,7 @@
|
||||
"Distance to city center": "Distance to city center",
|
||||
"Distance to hotel: {distanceInM} m": "Distance to hotel: {distanceInM} m",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?",
|
||||
"Don't cancel": "Don't cancel",
|
||||
"Done": "Done",
|
||||
"Download invoice": "Download invoice",
|
||||
"Download the Scandic app": "Download the Scandic app",
|
||||
@@ -203,6 +214,7 @@
|
||||
"Extra bed will be provided additionally": "Extra bed will be provided additionally",
|
||||
"Extras to your booking": "Extras to your booking",
|
||||
"FAQ": "FAQ",
|
||||
"Failed to add to calendar": "Failed to add to calendar",
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
@@ -247,6 +259,7 @@
|
||||
"Go to My Benefits": "Go to My Benefits",
|
||||
"Go to profile": "Go to profile",
|
||||
"Guarantee booking with credit card": "Guarantee booking with credit card",
|
||||
"Guarantee late arrival": "Guarantee late arrival",
|
||||
"Guest information": "Guest information",
|
||||
"Guests": "Guests",
|
||||
"Guests & Rooms": "Guests & Rooms",
|
||||
@@ -298,6 +311,7 @@
|
||||
"Join now": "Join now",
|
||||
"Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.",
|
||||
"Kayaking": "Kayaking",
|
||||
"Keep stay": "Keep stay",
|
||||
"King bed": "King bed",
|
||||
"Language": "Language",
|
||||
"Last name": "Last name",
|
||||
@@ -392,6 +406,7 @@
|
||||
"No": "No",
|
||||
"No availability": "No availability",
|
||||
"No breakfast": "No breakfast",
|
||||
"No charges were made.": "No charges were made.",
|
||||
"No content published": "No content published",
|
||||
"No hotels match your filters": "No hotels match your filters",
|
||||
"No matching location found": "No matching location found",
|
||||
@@ -694,6 +709,7 @@
|
||||
"Your points to spend": "Your points to spend",
|
||||
"Your room": "Your room",
|
||||
"Your selected bed type will be provided based on availability": "Your selected bed type will be provided based on availability",
|
||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out",
|
||||
"Zip code": "Zip code",
|
||||
"Zoo": "Zoo",
|
||||
"Zoom in": "Zoom in",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
|
||||
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)",
|
||||
"<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult": "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/yötä aikuista kohti",
|
||||
"<strong>Status</strong> Paid": "<strong>Status</strong> Maksettu",
|
||||
"<strong>Status</strong> Unpaid": "<strong>Status</strong> Ei maksettu",
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.",
|
||||
"A photo of the room": "Kuva huoneesta",
|
||||
"ACCE": "Saavutettavuus",
|
||||
@@ -46,6 +48,8 @@
|
||||
"Apply": "Tallenna",
|
||||
"Approx.": "N.",
|
||||
"Approx. {value}": "N. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Oletko varmasti haluamassa peruuttaa majoituksesi hoteleissa {hotel} alkaen {checkInDate} asti {checkOutDate}? Tätä ei voi kumota.",
|
||||
"Are you sure you want to continue with the cancellation?": "Oletko varmasti haluamassa jatkaa peruuttamista?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?",
|
||||
"Arrival date": "Saapumispäivä",
|
||||
"As our Close Friend": "Läheisenä ystävänämme",
|
||||
@@ -100,7 +104,11 @@
|
||||
"Cabaret seating": "Cabaret seating",
|
||||
"Campaign": "Kampanja",
|
||||
"Cancel": "Peruuttaa",
|
||||
"Cancel stay": "Peruuta majoitus",
|
||||
"Cancellation cost": "Peruutusmaksu",
|
||||
"Cancellation number": "Peruutusnumero",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Cancelled": "Peruttu",
|
||||
"Change room": "Vaihda huonetta",
|
||||
"Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Muutoksia voi tehdä {time} päivänä {date}, olettaen saatavuuden olemassaolon. Huonehinnat voivat muuttua.",
|
||||
"Check for level upgrade": "Check for level upgrade",
|
||||
@@ -138,6 +146,7 @@
|
||||
"Complete booking": "Complete booking",
|
||||
"Complete booking & go to payment": "Täydennä varaus & siirry maksamaan",
|
||||
"Complete the booking": "Täydennä varaus",
|
||||
"Confirm cancellation": "Vahvista peruutus",
|
||||
"Contact information": "Yhteystiedot",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
"Contact us": "Ota meihin yhteyttä",
|
||||
@@ -155,6 +164,7 @@
|
||||
"Current Points: {points, number}": "Current Points: {points, number}",
|
||||
"Current password": "Nykyinen salasana",
|
||||
"Customer service": "Asiakaspalvelu",
|
||||
"Customer support": "Asiakaspalvelu",
|
||||
"Date of Birth": "Syntymäaika",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Päivä",
|
||||
@@ -175,6 +185,7 @@
|
||||
"Distance to city center": "Etäisyys kaupungin keskustaan",
|
||||
"Distance to hotel: {distanceInM} m": "Etäisyys hotelliin: {distance} m",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?",
|
||||
"Don't cancel": "Peruuta ei",
|
||||
"Done": "Valmis",
|
||||
"Download invoice": "Lataa lasku",
|
||||
"Download the Scandic app": "Lataa Scandic-sovellus",
|
||||
@@ -200,6 +211,7 @@
|
||||
"Extra bed will be provided additionally": "Lisävuode toimitetaan erikseen",
|
||||
"Extras to your booking": "Varauksessa lisäpalveluita",
|
||||
"FAQ": "Usein kysytyt kysymykset",
|
||||
"Failed to add to calendar": "Virhe kalenteriin lisäämisessä",
|
||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
@@ -244,6 +256,7 @@
|
||||
"Go to My Benefits": "Siirry kohtaan 'Omat edut'",
|
||||
"Go to profile": "Go to profile",
|
||||
"Guarantee booking with credit card": "Varmista varaus luottokortilla",
|
||||
"Guarantee late arrival": "Varmista myöhäisempi tulema",
|
||||
"Guest information": "Vieraan tiedot",
|
||||
"Guests": "Vierailijat",
|
||||
"Guests & Rooms": "Vieraat & Huoneet",
|
||||
@@ -294,6 +307,7 @@
|
||||
"Join now": "Liity jäseneksi",
|
||||
"Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.",
|
||||
"Kayaking": "Melonta",
|
||||
"Keep stay": "Jatka majoittumista",
|
||||
"King bed": "King-vuode",
|
||||
"Language": "Kieli",
|
||||
"Last name": "Sukunimi",
|
||||
@@ -355,6 +369,7 @@
|
||||
"Menu": "Valikko",
|
||||
"Menus": "Valikot",
|
||||
"Modify": "Muokkaa",
|
||||
"Modify dates": "Muuta päivämääriä",
|
||||
"Modify guest details": "Muuta vierailijoiden tietoja",
|
||||
"Mon-Fri Always open": "Ma-Pe Aina auki",
|
||||
"Mon-Fri {openingTime}-{closingTime}": "Ma-Pe {openingTime}-{closingTime}",
|
||||
@@ -387,6 +402,7 @@
|
||||
"No": "Ei",
|
||||
"No availability": "Ei saatavuutta",
|
||||
"No breakfast": "Ei aamiaista",
|
||||
"No charges were made.": "Ei maksuja tehty.",
|
||||
"No content published": "Ei julkaistua sisältöä",
|
||||
"No hotels match your filters": "Yksikään huone ei vastannut suodattimiasi",
|
||||
"No matching location found": "Vastaavaa sijaintia ei löytynyt",
|
||||
@@ -688,6 +704,7 @@
|
||||
"Your points to spend": "Käytettävissä olevat pisteesi",
|
||||
"Your room": "Sinun huoneesi",
|
||||
"Your selected bed type will be provided based on availability": "Valitun vuodetyypin toimitetaan saatavuuden mukaan",
|
||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Majoituksesi peruutettiin. Peruutusmaksu: 0 {currency}. Emme voi käyttää sitä, että suunnitellut majoitukset eivät toiminneet",
|
||||
"Zip code": "Postinumero",
|
||||
"Zoo": "Eläintarha",
|
||||
"Zoom in": "Lähennä",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
|
||||
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)",
|
||||
"<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult": "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/natt per voksen",
|
||||
"<strong>Status</strong> Paid": "<strong>Status</strong> Betalt",
|
||||
"<strong>Status</strong> Unpaid": "<strong>Status</strong> Ikke betalt",
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.",
|
||||
"A photo of the room": "Et bilde av rommet",
|
||||
"ACCE": "Tilgjengelighet",
|
||||
@@ -46,6 +48,8 @@
|
||||
"Apply": "Velg",
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Ca. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Er du sikker på, at du vil annullere dit ophold hos {hotel} fra {checkInDate} til {checkOutDate}? Dette kan ikke gendannes.",
|
||||
"Are you sure you want to continue with the cancellation?": "Er du sikker på, at du vil fortsætte med annullereringen?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"As our Close Friend": "Som vår nære venn",
|
||||
@@ -100,7 +104,11 @@
|
||||
"Cabaret seating": "Cabaret seating",
|
||||
"Campaign": "Kampanje",
|
||||
"Cancel": "Avbryt",
|
||||
"Cancel stay": "Avbryt opphold",
|
||||
"Cancellation cost": "Annulleret pris",
|
||||
"Cancellation number": "Annulleringsnummer",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Cancelled": "Avbrutt",
|
||||
"Change room": "Endre rom",
|
||||
"Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Endringer kan gjøres til {time} på {date}, under forutsetning av tilgjengelighet. Rompriser kan variere.",
|
||||
"Check for level upgrade": "Check for level upgrade",
|
||||
@@ -137,6 +145,7 @@
|
||||
"Complete booking": "Fullfør reservasjonen",
|
||||
"Complete booking & go to payment": "Fullfør bestilling & gå til betaling",
|
||||
"Complete the booking": "Fullfør reservasjonen",
|
||||
"Confirm cancellation": "Bekræft annullerering",
|
||||
"Contact information": "Kontaktinformasjon",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
"Contact us": "Kontakt oss",
|
||||
@@ -154,6 +163,7 @@
|
||||
"Current Points: {points, number}": "Current Points: {points, number}",
|
||||
"Current password": "Nåværende passord",
|
||||
"Customer service": "Kundeservice",
|
||||
"Customer support": "Kundesupport",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Dag",
|
||||
@@ -174,6 +184,7 @@
|
||||
"Distance to city center": "Avstand til sentrum",
|
||||
"Distance to hotel: {distanceInM} m": "Avstand til hotell: {distance} m",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?",
|
||||
"Don't cancel": "Annuller ikke",
|
||||
"Done": "Ferdig",
|
||||
"Download invoice": "Last ned faktura",
|
||||
"Download the Scandic app": "Last ned Scandic-appen",
|
||||
@@ -199,6 +210,7 @@
|
||||
"Extra bed will be provided additionally": "Ekstra seng vil bli tilgjengelig",
|
||||
"Extras to your booking": "Tilvalg til bestillingen din",
|
||||
"FAQ": "Ofte stilte spørsmål",
|
||||
"Failed to add to calendar": "Feil ved tilføyelse til kalender",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
@@ -243,6 +255,7 @@
|
||||
"Go to My Benefits": "Gå til 'Mine fordeler'",
|
||||
"Go to profile": "Go to profile",
|
||||
"Guarantee booking with credit card": "Garantere booking med kredittkort",
|
||||
"Guarantee late arrival": "Garantere sen ankomst",
|
||||
"Guest information": "Informasjon til gjester",
|
||||
"Guests": "Gjester",
|
||||
"Guests & Rooms": "Gjester & rom",
|
||||
@@ -293,6 +306,7 @@
|
||||
"Join now": "Bli medlem nå",
|
||||
"Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.",
|
||||
"Kayaking": "Kajakkpadling",
|
||||
"Keep stay": "Behold ophold",
|
||||
"King bed": "King-size-seng",
|
||||
"Language": "Språk",
|
||||
"Last name": "Etternavn",
|
||||
@@ -354,6 +368,7 @@
|
||||
"Menu": "Menu",
|
||||
"Menus": "Menyer",
|
||||
"Modify": "Endre",
|
||||
"Modify dates": "Endre datoer",
|
||||
"Modify guest details": "Endre gjestdetaljer",
|
||||
"Mon-Fri Always open": "Man-Fre Alltid åpen",
|
||||
"Mon-Fri {openingTime}-{closingTime}": "Man-Fre {openingTime}-{closingTime}",
|
||||
@@ -386,6 +401,7 @@
|
||||
"No": "Nei",
|
||||
"No availability": "Ingen tilgjengelighet",
|
||||
"No breakfast": "Ingen frokost",
|
||||
"No charges were made.": "Ingen gebyrer blev opkrævet.",
|
||||
"No content published": "Ingen innhold publisert",
|
||||
"No hotels match your filters": "Ingen rom samsvarte med filtrene dine",
|
||||
"No matching location found": "Fant ingen samsvarende plassering",
|
||||
@@ -686,6 +702,7 @@
|
||||
"Your points to spend": "Dine brukbare poeng",
|
||||
"Your room": "Rommet ditt",
|
||||
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Dit ophold blev annulleret. Annullereringspris: 0 {currency}. Vi beklager, at planerne ikke fungerede ud",
|
||||
"Zip code": "Post kode",
|
||||
"Zoo": "Dyrehage",
|
||||
"Zoom in": "Zoom inn",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
|
||||
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)",
|
||||
"<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/night per adult": "<strikethrough>{amount}</strikethrough> <free>0 {currency}</free>/natt per vuxen",
|
||||
"<strong>Status</strong> Paid": "<strong>Status</strong> Betalat",
|
||||
"<strong>Status</strong> Unpaid": "<strong>Status</strong> Ej betalat",
|
||||
"A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.",
|
||||
"A photo of the room": "Ett foto av rummet",
|
||||
"ACCE": "Tillgänglighet",
|
||||
@@ -46,6 +48,8 @@
|
||||
"Apply": "Tillämpa",
|
||||
"Approx.": "Ca.",
|
||||
"Approx. {value}": "Ca. {value}",
|
||||
"Are you sure you want to cancel your stay at {hotel} from {checkInDate} to {checkOutDate}? This can't be reversed.": "Är du säker på att du vill avboka din vistelse hos {hotel} från {checkInDate} till {checkOutDate}? Detta kan inte ångras.",
|
||||
"Are you sure you want to continue with the cancellation?": "Är du säker på att du vill fortsätta med avbokningen?",
|
||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?",
|
||||
"Arrival date": "Ankomstdatum",
|
||||
"As our Close Friend": "Som vår nära vän",
|
||||
@@ -100,7 +104,11 @@
|
||||
"Cabaret seating": "Cabaret seating",
|
||||
"Campaign": "Kampanj",
|
||||
"Cancel": "Avbryt",
|
||||
"Cancel stay": "Avboka vistelse",
|
||||
"Cancellation cost": "Avbokningskostnad",
|
||||
"Cancellation number": "Avbokningsnummer",
|
||||
"Cancellation policy": "Cancellation policy",
|
||||
"Cancelled": "Avbokad",
|
||||
"Change room": "Ändra rum",
|
||||
"Changes can be made until {time} on {date}, subject to availability. Room rates may vary.": "Ändringar kan göras tills {time} den {date}, under förutsättning av tillgänglighet. Priserna för rummen kan variera.",
|
||||
"Check for level upgrade": "Check for level upgrade",
|
||||
@@ -137,6 +145,7 @@
|
||||
"Complete booking": "Slutför bokning",
|
||||
"Complete booking & go to payment": "Fullför bokning & gå till betalning",
|
||||
"Complete the booking": "Slutför bokningen",
|
||||
"Confirm cancellation": "Bekräfta avbokning",
|
||||
"Contact information": "Kontaktinformation",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
"Contact us": "Kontakta oss",
|
||||
@@ -154,6 +163,7 @@
|
||||
"Current Points: {points, number}": "Current Points: {points, number}",
|
||||
"Current password": "Nuvarande lösenord",
|
||||
"Customer service": "Kundservice",
|
||||
"Customer support": "Kundesupport",
|
||||
"Date of Birth": "Födelsedatum",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Dag",
|
||||
@@ -174,6 +184,7 @@
|
||||
"Distance to city center": "Avstånd till centrum",
|
||||
"Distance to hotel: {distanceInM} m": "Avstånd till hotell: {distance} m",
|
||||
"Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?",
|
||||
"Don't cancel": "Avboka inte",
|
||||
"Done": "Klar",
|
||||
"Download invoice": "Ladda ner faktura",
|
||||
"Download the Scandic app": "Ladda ner Scandic-appen",
|
||||
@@ -199,6 +210,7 @@
|
||||
"Extra bed will be provided additionally": "Extra säng kommer att tillhandahållas",
|
||||
"Extras to your booking": "Extra tillval till din bokning",
|
||||
"FAQ": "FAQ",
|
||||
"Failed to add to calendar": "Misslyckades att lägga till i kalender",
|
||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||
"Failed to unlink account": "Failed to unlink account",
|
||||
"Failed to upgrade level": "Failed to upgrade level",
|
||||
@@ -243,6 +255,7 @@
|
||||
"Go to My Benefits": "Gå till 'Mina förmåner'",
|
||||
"Go to profile": "Go to profile",
|
||||
"Guarantee booking with credit card": "Garantera bokning med kreditkort",
|
||||
"Guarantee late arrival": "Garantera sen ankomst",
|
||||
"Guest information": "Information till gästerna",
|
||||
"Guests": "Gäster",
|
||||
"Guests & Rooms": "Gäster & rum",
|
||||
@@ -293,6 +306,7 @@
|
||||
"Join now": "Gå med nu",
|
||||
"Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.",
|
||||
"Kayaking": "Kajakpaddling",
|
||||
"Keep stay": "Behåll vistelse",
|
||||
"King bed": "King size-säng",
|
||||
"Language": "Språk",
|
||||
"Last name": "Efternamn",
|
||||
@@ -354,6 +368,7 @@
|
||||
"Menu": "Meny",
|
||||
"Menus": "Menyer",
|
||||
"Modify": "Ändra",
|
||||
"Modify dates": "Ändra datum",
|
||||
"Modify guest details": "Ändra gästinformation",
|
||||
"Mon-Fri Always open": "Mån-Fre Alltid öppet",
|
||||
"Mon-Fri {openingTime}-{closingTime}": "Mån-Fre {openingTime}-{closingTime}",
|
||||
@@ -386,6 +401,7 @@
|
||||
"No": "Nej",
|
||||
"No availability": "Ingen tillgänglighet",
|
||||
"No breakfast": "Ingen frukost",
|
||||
"No charges were made.": "Inga avgifter har debiterats.",
|
||||
"No content published": "Inget innehåll publicerat",
|
||||
"No hotels match your filters": "Inga rum matchade dina filter",
|
||||
"No matching location found": "Ingen matchande plats hittades",
|
||||
@@ -686,6 +702,7 @@
|
||||
"Your points to spend": "Dina spenderbara poäng",
|
||||
"Your room": "Ditt rum",
|
||||
"Your selected bed type will be provided based on availability": "Din valda sängtyp kommer att tillhandahållas baserat på tillgänglighet",
|
||||
"Your stay was cancelled. Cancellation cost: 0 {currency}. We’re sorry to see that the plans didn’t work out": "Din vistelse blev avbokad. Avbokningskostnad: 0 {currency}. Vi beklagar att planerna inte fungerade ut",
|
||||
"Zip code": "Postnummer",
|
||||
"Zoo": "Djurpark",
|
||||
"Zoom in": "Zooma in",
|
||||
|
||||
@@ -89,6 +89,11 @@ export const priceChangeInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
|
||||
export const cancelBookingInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
||||
})
|
||||
|
||||
// Query
|
||||
const confirmationNumberInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
|
||||
@@ -6,7 +6,11 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc"
|
||||
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import { createBookingInput, priceChangeInput } from "./input"
|
||||
import {
|
||||
cancelBookingInput,
|
||||
createBookingInput,
|
||||
priceChangeInput,
|
||||
} from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
|
||||
import type { Session } from "next-auth"
|
||||
@@ -27,6 +31,13 @@ const priceChangeSuccessCounter = meter.createCounter(
|
||||
const priceChangeFailCounter = meter.createCounter(
|
||||
"trpc.bookings.price-change-fail"
|
||||
)
|
||||
const cancelBookingCounter = meter.createCounter("trpc.bookings.cancel")
|
||||
const cancelBookingSuccessCounter = meter.createCounter(
|
||||
"trpc.bookings.cancel-success"
|
||||
)
|
||||
const cancelBookingFailCounter = meter.createCounter(
|
||||
"trpc.bookings.cancel-fail"
|
||||
)
|
||||
|
||||
async function getMembershipNumber(
|
||||
session: Session | null
|
||||
@@ -201,6 +212,98 @@ export const bookingMutationRouter = router({
|
||||
|
||||
priceChangeSuccessCounter.add(1, { confirmationNumber })
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
cancel: safeProtectedServiceProcedure
|
||||
.input(cancelBookingInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { confirmationNumber, language } = input
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
|
||||
const cancellationReason = {
|
||||
reasonCode: "WEB-CANCEL",
|
||||
reason: "WEB-CANCEL",
|
||||
}
|
||||
|
||||
const loggingAttributes = {
|
||||
confirmationNumber,
|
||||
language,
|
||||
}
|
||||
|
||||
cancelBookingCounter.add(1, loggingAttributes)
|
||||
|
||||
console.info(
|
||||
"api.booking.cancel start",
|
||||
JSON.stringify({
|
||||
request: loggingAttributes,
|
||||
headers,
|
||||
})
|
||||
)
|
||||
|
||||
const apiResponse = await api.remove(
|
||||
api.endpoints.v1.Booking.cancel(confirmationNumber),
|
||||
{
|
||||
headers,
|
||||
body: JSON.stringify(cancellationReason),
|
||||
} as RequestInit,
|
||||
{ language }
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
cancelBookingFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.cancel error",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
|
||||
if (!verifiedData.success) {
|
||||
cancelBookingFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.booking.cancel validation error",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
cancelBookingSuccessCounter.add(1, loggingAttributes)
|
||||
|
||||
console.info(
|
||||
"api.booking.cancel success",
|
||||
JSON.stringify({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -136,6 +136,49 @@ export const linkedReservationsSchema = z.object({
|
||||
profileId: z.string().default(""),
|
||||
})
|
||||
|
||||
const linksSchema = z.object({
|
||||
addAncillary: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
meta: z.object({
|
||||
method: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable(),
|
||||
cancel: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
meta: z.object({
|
||||
method: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable(),
|
||||
guarantee: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
meta: z.object({
|
||||
method: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable(),
|
||||
modify: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
meta: z.object({
|
||||
method: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable(),
|
||||
self: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
meta: z.object({
|
||||
method: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export const bookingConfirmationSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
@@ -167,20 +210,12 @@ export const bookingConfirmationSchema = z
|
||||
}),
|
||||
id: z.string(),
|
||||
type: z.literal("booking"),
|
||||
links: z.object({
|
||||
addAncillary: z
|
||||
.object({
|
||||
href: z.string(),
|
||||
meta: z.object({
|
||||
method: z.string(),
|
||||
}),
|
||||
})
|
||||
.nullable(),
|
||||
}),
|
||||
links: linksSchema,
|
||||
}),
|
||||
})
|
||||
.transform(({ data }) => ({
|
||||
...data.attributes,
|
||||
extraBedTypes: data.attributes.childBedPreferences,
|
||||
showAncillaries: !!data.links.addAncillary,
|
||||
isCancelable: !!data.links.cancel,
|
||||
isModifiable: !!data.links.modify,
|
||||
}))
|
||||
|
||||
@@ -69,6 +69,12 @@ export const bookingQueryRouter = router({
|
||||
})
|
||||
)
|
||||
|
||||
// If the booking is not found, return null.
|
||||
// This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them.
|
||||
if (apiResponse.status === 400) {
|
||||
return null
|
||||
}
|
||||
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ import type { EventAttributes } from "ics"
|
||||
import type { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export interface AddToCalendarProps {
|
||||
checkInDate: RouterOutput["booking"]["confirmation"]["booking"]["checkInDate"]
|
||||
checkInDate: NonNullable<
|
||||
RouterOutput["booking"]["confirmation"]
|
||||
>["booking"]["checkInDate"]
|
||||
event: EventAttributes
|
||||
hotelName: RouterOutput["booking"]["confirmation"]["hotel"]["name"]
|
||||
hotelName: NonNullable<
|
||||
RouterOutput["booking"]["confirmation"]
|
||||
>["hotel"]["name"]
|
||||
renderButton: (onPress: () => Promise<void>) => React.ReactNode
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export enum IconName {
|
||||
Cool = "Cool",
|
||||
CroissantCoffeeEgg = "CroissantCoffeeEgg",
|
||||
CrossCircle = "CrossCircle",
|
||||
CrossCircleOutline = "CrossCircleOutline",
|
||||
Cultural = "Cultural",
|
||||
CutleryOne = "CutleryOne",
|
||||
CutleryTwo = "CutleryTwo",
|
||||
|
||||
Reference in New Issue
Block a user