Merged in feat/SW-1281-ancillaries-add-flow (pull request #1399)
Feat/SW-1281 ancillaries add flow * feat(SW-1546): update design * feat(SW-1546): show points only if logged in * feat(SW-1546): always show points * feat(SW-1281): ancillary add flow initial * feat(SW-1546): add api call * feat(SW-1281): refactor naming and break out components * feat(SW-1281): handle back button * feat(SW-1281): make mobile cards clickable * feat(SW-1281): refactor spread ancillaries * feat(SW-1281): add deliverytimes * feat(SW-1281): rebase master * feat(SW-1281): add design for logged in or not * feat(SW-1281): add design * feat(SW-1281): add mobile design * feat(SW-1281): fix carousel * feat(SW-1281): show deliverytime only if ancillary has not been added * feat(SW-1281): add design * feat(SW-1281): add translations * feat(SW-1281): add translations * feat(SW-1281): add translations * feat(SW-1281): base dates on check in date only * feat(SW-1281): fix show correct toast when no valid data * feat(SW-1281): hande logic if deliverytime is not required * feat(SW-1281): fix max width for mobile * feat(SW-1281): refactor after pr comment Approved-by: Niclas Edenvin Approved-by: Linus Flood
This commit is contained in:
committed by
Linus Flood
parent
341f0c54ed
commit
541b91e34c
@@ -0,0 +1,71 @@
|
||||
.modalWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 80dvh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modalScrollable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
margin-top: var(--Spacing-x1);
|
||||
flex-shrink: 0;
|
||||
margin-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x4);
|
||||
justify-content: flex-end;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
background: var(--UI-Opacity-White-100);
|
||||
padding-top: var(--Spacing-x2);
|
||||
border-top: 1px solid var(--Base-Border-Normal);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modalWrapper {
|
||||
width: 492px;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import { generateDeliveryOptions } from "../../utils"
|
||||
import ConfirmationStep from "../ConfirmationStep"
|
||||
import DeliveryMethodStep from "../DeliveryDetailsStep"
|
||||
import { type AncillaryFormData, ancillaryFormSchema } from "../schema"
|
||||
import SelectQuantityStep from "../SelectQuantityStep"
|
||||
|
||||
import styles from "./addAncillaryFlowModal.module.css"
|
||||
|
||||
import type { AddAncillaryFlowModalProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
type FieldName = keyof AncillaryFormData
|
||||
const STEP_FIELD_MAP: Record<number, FieldName[]> = {
|
||||
1: ["quantityWithPoints", "quantityWithCard"],
|
||||
2: ["deliveryTime"],
|
||||
3: ["termsAndConditions"],
|
||||
}
|
||||
|
||||
export default function AddAncillaryFlowModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
booking,
|
||||
user,
|
||||
}: AddAncillaryFlowModalProps) {
|
||||
const {
|
||||
step,
|
||||
nextStep,
|
||||
prevStep,
|
||||
resetStore,
|
||||
selectedAncillary,
|
||||
confirmationNumber,
|
||||
openedFrom,
|
||||
setGridIsOpen,
|
||||
} = useAddAncillaryStore()
|
||||
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const isMobile = useMediaQuery("(max-width: 767px)")
|
||||
|
||||
const deliveryTimeOptions = generateDeliveryOptions(booking.checkInDate)
|
||||
|
||||
const defaultDeliveryTime = deliveryTimeOptions[0]?.value
|
||||
|
||||
const formMethods = useForm<AncillaryFormData>({
|
||||
defaultValues: {
|
||||
quantityWithPoints: null,
|
||||
quantityWithCard: user ? null : 1,
|
||||
deliveryTime: defaultDeliveryTime,
|
||||
optionalText: "",
|
||||
termsAndConditions: false,
|
||||
},
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onChange",
|
||||
resolver: zodResolver(ancillaryFormSchema),
|
||||
})
|
||||
|
||||
const { reset, trigger, handleSubmit, formState } = formMethods
|
||||
|
||||
const addAncillary = trpc.booking.packages.useMutation({
|
||||
onSuccess: (data, variables) => {
|
||||
if (!data) {
|
||||
toast.error(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "Something went wrong. {ancillary} could not be added to your booking!",
|
||||
},
|
||||
{ ancillary: selectedAncillary?.title }
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
const description = variables.ancillaryDeliveryTime
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "Delivery between {deliveryTime}. Payment will be made on check-in.",
|
||||
},
|
||||
{ deliveryTime: variables.ancillaryDeliveryTime }
|
||||
)
|
||||
: undefined
|
||||
|
||||
toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "{ancillary} added to your booking!" },
|
||||
{ ancillary: selectedAncillary?.title }
|
||||
),
|
||||
{ description }
|
||||
)
|
||||
handleClose()
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
intl.formatMessage(
|
||||
{
|
||||
id: "Something went wrong. {ancillary} could not be added to your booking!",
|
||||
},
|
||||
{ ancillary: selectedAncillary?.title }
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (data: AncillaryFormData) => {
|
||||
const packages = []
|
||||
if (data.quantityWithCard) {
|
||||
packages.push({
|
||||
code: selectedAncillary!.id,
|
||||
quantity: data.quantityWithCard,
|
||||
comment: data.optionalText || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
if (selectedAncillary?.loyaltyCode && data.quantityWithPoints) {
|
||||
packages.push({
|
||||
code: selectedAncillary.loyaltyCode,
|
||||
quantity: data.quantityWithPoints,
|
||||
comment: data.optionalText || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
addAncillary.mutate({
|
||||
confirmationNumber,
|
||||
ancillaryComment: data.optionalText ?? "",
|
||||
ancillaryDeliveryTime: data.deliveryTime ?? undefined,
|
||||
packages,
|
||||
language: lang,
|
||||
})
|
||||
}
|
||||
|
||||
const handleNextStep = async () => {
|
||||
let fieldsToValidate = []
|
||||
|
||||
if (isMobile && step === 1) {
|
||||
fieldsToValidate = [...STEP_FIELD_MAP[1]]
|
||||
if (selectedAncillary?.requiresDeliveryTime) {
|
||||
fieldsToValidate = [...fieldsToValidate, ...STEP_FIELD_MAP[2]]
|
||||
}
|
||||
} else if (step === 2) {
|
||||
fieldsToValidate = selectedAncillary?.requiresDeliveryTime
|
||||
? STEP_FIELD_MAP[2] || []
|
||||
: []
|
||||
} else {
|
||||
fieldsToValidate = STEP_FIELD_MAP[step] || []
|
||||
}
|
||||
|
||||
if (await trigger(fieldsToValidate)) {
|
||||
nextStep()
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (step > 1) {
|
||||
prevStep()
|
||||
} else {
|
||||
handleClose()
|
||||
if (openedFrom === "grid") setGridIsOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
reset()
|
||||
resetStore()
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!selectedAncillary) return null
|
||||
|
||||
const confirmLabel = intl.formatMessage({ id: "Confirm" })
|
||||
const continueLabel = intl.formatMessage({ id: "Continue" })
|
||||
const confirmStep =
|
||||
isMobile || (!isMobile && !selectedAncillary.requiresDeliveryTime) ? 2 : 3
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onToggle={handleClose}
|
||||
title={selectedAncillary.title}
|
||||
>
|
||||
<div className={styles.modalWrapper}>
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
|
||||
<div className={styles.modalScrollable}>
|
||||
<div className={styles.imageContainer}>
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={selectedAncillary.imageUrl}
|
||||
alt={selectedAncillary.title}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.contentContainer}>
|
||||
<div className={styles.price}>
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{formatPrice(
|
||||
intl,
|
||||
selectedAncillary.price.total,
|
||||
selectedAncillary.price.currency
|
||||
)}
|
||||
</Body>
|
||||
{selectedAncillary.points && (
|
||||
<>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{selectedAncillary.points}{" "}
|
||||
{intl.formatMessage({ id: "points" })}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{selectedAncillary.description && (
|
||||
<Body asChild color="uiTextHighContrast">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: selectedAncillary.description,
|
||||
}}
|
||||
/>
|
||||
</Body>
|
||||
)}
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<SelectQuantityStep user={user} />
|
||||
{selectedAncillary.requiresDeliveryTime && (
|
||||
<DeliveryMethodStep
|
||||
deliveryTimeOptions={deliveryTimeOptions}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{step === 2 && <ConfirmationStep />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{step === 1 && <SelectQuantityStep user={user} />}
|
||||
{step === 2 && selectedAncillary.requiresDeliveryTime && (
|
||||
<DeliveryMethodStep
|
||||
deliveryTimeOptions={deliveryTimeOptions}
|
||||
/>
|
||||
)}
|
||||
{(step === 3 ||
|
||||
(step === 2 &&
|
||||
!selectedAncillary.requiresDeliveryTime)) && (
|
||||
<ConfirmationStep />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.actionButtons}>
|
||||
<Button
|
||||
type="button"
|
||||
theme="base"
|
||||
intent="text"
|
||||
size="small"
|
||||
onClick={handleBack}
|
||||
>
|
||||
{intl.formatMessage({ id: "Back" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
intent={step === confirmStep ? "primary" : "secondary"}
|
||||
size="small"
|
||||
disabled={formState.isSubmitting}
|
||||
onClick={
|
||||
step === confirmStep
|
||||
? () => handleSubmit(onSubmit)()
|
||||
: handleNextStep
|
||||
}
|
||||
>
|
||||
{step === confirmStep ? confirmLabel : continueLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x-one-and-half);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
bookingTermsAndConditions,
|
||||
privacyPolicy,
|
||||
} from "@/constants/currentWebHrefs"
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import { CreditCard } from "@/components/Icons"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
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 { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./confirmationStep.module.css"
|
||||
|
||||
export default function ConfirmationStep() {
|
||||
const { watch } = useFormContext()
|
||||
const { selectedAncillary } = useAddAncillaryStore()
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const quantityWithPoints = watch("quantityWithPoints")
|
||||
const quantityWithCard = watch("quantityWithCard")
|
||||
|
||||
if (!selectedAncillary) {
|
||||
return null
|
||||
}
|
||||
|
||||
const totalPrice = quantityWithCard
|
||||
? selectedAncillary.price.total * quantityWithCard
|
||||
: null
|
||||
|
||||
const totalPoints =
|
||||
quantityWithPoints && selectedAncillary.points
|
||||
? selectedAncillary.points * quantityWithPoints
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<header>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({
|
||||
id: "Reserve with Card",
|
||||
})}
|
||||
</Subtitle>
|
||||
</header>
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.",
|
||||
})}
|
||||
</Body>
|
||||
<div className={styles.card}>
|
||||
<CreditCard color="black" />
|
||||
<Body textTransform="bold">{"MasterCard"}</Body>
|
||||
<Body color="uiTextMediumContrast">{"**** 1234"}</Body>
|
||||
</div>
|
||||
<Checkbox name="termsAndConditions" registerOptions={{ required: true }}>
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
|
||||
<div className={styles.price}>
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{ id: "<b>Total price</b> (incl VAT)" },
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)}
|
||||
</Caption>
|
||||
{totalPrice !== null && (
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{formatPrice(intl, totalPrice, selectedAncillary.price.currency)}
|
||||
</Body>
|
||||
)}
|
||||
{totalPoints !== null && (
|
||||
<div>
|
||||
<div>
|
||||
<Divider variant="vertical" color="subtle" />
|
||||
</div>
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{totalPoints} {intl.formatMessage({ id: "points" })}
|
||||
</Body>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.selectContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./deliveryDetailsStep.module.css"
|
||||
|
||||
import type { DeliveryMethodStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function DeliveryMethodStep({
|
||||
deliveryTimeOptions,
|
||||
}: DeliveryMethodStepProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.selectContainer}>
|
||||
<div className={styles.select}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({ id: "Delivered at:" })}
|
||||
</Subtitle>
|
||||
<Select
|
||||
name="deliveryTime"
|
||||
label={""}
|
||||
items={deliveryTimeOptions}
|
||||
registerOptions={{ required: true }}
|
||||
isNestedInModal
|
||||
/>
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
<div className={styles.select}>
|
||||
<Input
|
||||
label={intl.formatMessage({ id: "Other Requests" })}
|
||||
name="optionalText"
|
||||
/>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "Optional",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import { DiamondIcon } from "@/components/Icons"
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import styles from "./selectQuantityStep.module.css"
|
||||
|
||||
import type { SelectQuantityStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
|
||||
const intl = useIntl()
|
||||
const { selectedAncillary } = useAddAncillaryStore()
|
||||
const { formState } = useFormContext()
|
||||
|
||||
const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
|
||||
label: `${i}`,
|
||||
value: i,
|
||||
}))
|
||||
|
||||
const pointsCost = selectedAncillary?.points ?? 0
|
||||
const currentPoints = user?.membership?.currentPoints ?? 0
|
||||
const maxAffordable =
|
||||
pointsCost > 0 ? Math.min(Math.floor(currentPoints / pointsCost), 7) : 0
|
||||
|
||||
const pointsQuantityOptions = Array.from(
|
||||
{ length: maxAffordable + 1 },
|
||||
(_, i) => ({
|
||||
label: `${i}`,
|
||||
value: i,
|
||||
})
|
||||
)
|
||||
|
||||
const insufficientPoints = currentPoints < pointsCost || currentPoints === 0
|
||||
|
||||
const pointsLabel =
|
||||
insufficientPoints && user
|
||||
? intl.formatMessage({ id: "Insufficient points" })
|
||||
: intl.formatMessage({ id: "Select quantity" })
|
||||
|
||||
return (
|
||||
<div className={styles.selectContainer}>
|
||||
{selectedAncillary?.points && user && (
|
||||
<div className={styles.select}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({ id: "Pay with points" })}
|
||||
</Subtitle>
|
||||
<div className={styles.totalPointsContainer}>
|
||||
<div className={styles.totalPoints}>
|
||||
<DiamondIcon />
|
||||
<Subtitle textTransform="uppercase" type="two">
|
||||
{intl.formatMessage({ id: "Total points" })}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Body>{currentPoints}</Body>
|
||||
</div>
|
||||
<Select
|
||||
name="quantityWithPoints"
|
||||
label={pointsLabel}
|
||||
items={pointsQuantityOptions}
|
||||
disabled={!user || insufficientPoints}
|
||||
isNestedInModal
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.select}>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage({ id: "Pay with Card" })}
|
||||
</Subtitle>
|
||||
<Select
|
||||
name="quantityWithCard"
|
||||
label={intl.formatMessage({ id: "Select quantity" })}
|
||||
items={cardQuantityOptions}
|
||||
isNestedInModal
|
||||
/>
|
||||
</div>
|
||||
<ErrorMessage errors={formState.errors} name="quantityWithCard" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
.selectContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-quarter);
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.totalPointsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--Scandic-Peach-10);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.totalPoints {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const ancillaryFormSchema = z
|
||||
.object({
|
||||
quantityWithPoints: z.number().nullable(),
|
||||
quantityWithCard: z.number().nullable(),
|
||||
deliveryTime: z.string().nullable().optional(),
|
||||
optionalText: z.string().optional(),
|
||||
termsAndConditions: z
|
||||
.boolean()
|
||||
.refine((val) => val, "You must accept the terms"),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
(data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0,
|
||||
{
|
||||
message: "You must select at least one quantity",
|
||||
path: ["quantityWithCard"],
|
||||
}
|
||||
)
|
||||
|
||||
export type AncillaryFormData = z.infer<typeof ancillaryFormSchema>
|
||||
@@ -0,0 +1,52 @@
|
||||
.modalTrigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x3) 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(251px, 1fr));
|
||||
gap: var(--Spacing-x2);
|
||||
height: 470px;
|
||||
overflow-y: auto;
|
||||
padding-right: var(--Spacing-x-one-and-half);
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border-radius: 28px;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
.chip.selected {
|
||||
background: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modalContent {
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1052px) {
|
||||
.modalContent {
|
||||
width: 833px;
|
||||
}
|
||||
|
||||
.modalTrigger {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./ancillaryGridModal.module.css"
|
||||
|
||||
import type {
|
||||
Ancillary,
|
||||
AncillaryGridModalProps,
|
||||
} from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function AncillaryGridModal({
|
||||
ancillaries,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
handleCardClick,
|
||||
}: AncillaryGridModalProps) {
|
||||
const intl = useIntl()
|
||||
const { isGridOpen, setGridIsOpen, setOpenedFrom } = useAddAncillaryStore()
|
||||
|
||||
const handleClick = (ancillary: Ancillary["ancillaryContent"][number]) => {
|
||||
handleCardClick(ancillary)
|
||||
setOpenedFrom("grid")
|
||||
setGridIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.modalTrigger}>
|
||||
<Button
|
||||
theme="base"
|
||||
variant="icon"
|
||||
intent="text"
|
||||
size="small"
|
||||
onClick={() => setGridIsOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({ id: "View all" })}
|
||||
<ChevronRightSmallIcon
|
||||
width={20}
|
||||
height={20}
|
||||
color="baseButtonTextOnFillNormal"
|
||||
/>
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isGridOpen}
|
||||
onToggle={() => setGridIsOpen(!isGridOpen)}
|
||||
title={intl.formatMessage({ id: "Upgrade your stay" })}
|
||||
>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={styles.tabs}>
|
||||
{ancillaries.map((category) => (
|
||||
<button
|
||||
key={category.categoryName}
|
||||
className={`${styles.chip} ${category.categoryName === selectedCategory ? styles.selected : ""}`}
|
||||
onClick={() => setSelectedCategory(category.categoryName)}
|
||||
>
|
||||
<Body
|
||||
color={
|
||||
category.categoryName === selectedCategory
|
||||
? "pale"
|
||||
: "baseTextHighContrast"
|
||||
}
|
||||
>
|
||||
{category.categoryName}
|
||||
</Body>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
{ancillaries
|
||||
.find((category) => category.categoryName === selectedCategory)
|
||||
?.ancillaryContent.map(({ description, ...ancillary }) => (
|
||||
<div
|
||||
key={ancillary.id}
|
||||
onClick={() => handleClick({ description, ...ancillary })}
|
||||
>
|
||||
<AncillaryCard key={ancillary.id} ancillary={ancillary} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,86 +2,26 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
margin: 0 auto;
|
||||
width: var(--max-width-content);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x3) 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(251px, 1fr));
|
||||
gap: var(--Spacing-x2);
|
||||
max-height: 417px;
|
||||
overflow-y: auto;
|
||||
padding-right: var(--Spacing-x1);
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border-radius: 28px;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
.chip.selected {
|
||||
background: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
.ancillaries {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.carouselContainer {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 80%;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modalContent {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.carouselContainer {
|
||||
grid-auto-columns: calc((100% - var(--Spacing-x3)) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1052px) {
|
||||
.mobileAncillaries {
|
||||
display: none;
|
||||
}
|
||||
.modalContent {
|
||||
width: 833px;
|
||||
}
|
||||
|
||||
.ancillaries {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(251px, 1fr));
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import { Carousel } from "@/components/Carousel"
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import AddAncillaryFlowModal from "./AddAncillaryFlow/AddAncillaryFlowModal"
|
||||
import AncillaryGridModal from "./AncillaryGridModal"
|
||||
|
||||
import styles from "./ancillaries.module.css"
|
||||
|
||||
import type {
|
||||
@@ -18,7 +19,7 @@ import type {
|
||||
Ancillary,
|
||||
} from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export function Ancillaries({ ancillaries }: AncillariesProps) {
|
||||
export function Ancillaries({ ancillaries, booking, user }: AncillariesProps) {
|
||||
const intl = useIntl()
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(
|
||||
() => {
|
||||
@@ -26,6 +27,10 @@ export function Ancillaries({ ancillaries }: AncillariesProps) {
|
||||
}
|
||||
)
|
||||
|
||||
const { setSelectedAncillary, setConfirmationNumber, setOpenedFrom } =
|
||||
useAddAncillaryStore()
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
|
||||
if (!ancillaries?.length) {
|
||||
return null
|
||||
}
|
||||
@@ -43,78 +48,79 @@ export function Ancillaries({ ancillaries }: AncillariesProps) {
|
||||
|
||||
const allAncillaries = mergeAncillaries(ancillaries)
|
||||
|
||||
const handleCardClick = (
|
||||
ancillary: Ancillary["ancillaryContent"][number]
|
||||
) => {
|
||||
if (booking?.confirmationNumber) {
|
||||
setConfirmationNumber(booking.confirmationNumber)
|
||||
}
|
||||
setSelectedAncillary(ancillary)
|
||||
setOpenedFrom("list")
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
<Title as="h5">{intl.formatMessage({ id: "Upgrade your stay" })}</Title>
|
||||
<div className={styles.modal}>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button theme="base" variant="icon" intent="text" size="small">
|
||||
{intl.formatMessage({ id: "View all" })}
|
||||
<ChevronRightSmallIcon
|
||||
width={20}
|
||||
height={20}
|
||||
color="baseButtonTextOnFillNormal"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
title={intl.formatMessage({ id: "Upgrade your stay" })}
|
||||
>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={styles.tabs}>
|
||||
{ancillaries.map((category) => (
|
||||
<button
|
||||
key={category.categoryName}
|
||||
className={`${styles.chip} ${category.categoryName === selectedCategory ? styles.selected : ""}`}
|
||||
onClick={() => setSelectedCategory(category.categoryName)}
|
||||
>
|
||||
<Caption
|
||||
color={
|
||||
category.categoryName === selectedCategory
|
||||
? "pale"
|
||||
: "baseTextHighContrast"
|
||||
}
|
||||
>
|
||||
{category.categoryName}
|
||||
</Caption>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
{ancillaries
|
||||
.find(
|
||||
(category) => category.categoryName === selectedCategory
|
||||
)
|
||||
?.ancillaryContent.map(({ description, ...ancillary }) => (
|
||||
<AncillaryCard key={ancillary.id} ancillary={ancillary} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
<AncillaryGridModal
|
||||
ancillaries={ancillaries}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
handleCardClick={handleCardClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.ancillaries}>
|
||||
{allAncillaries.slice(0, 4).map(({ description, ...ancillary }) => (
|
||||
<AncillaryCard key={ancillary.id} ancillary={ancillary} />
|
||||
))}
|
||||
{allAncillaries
|
||||
.slice(0, 4)
|
||||
.map(({ description, points, ...ancillary }) => {
|
||||
const ancillaryData = !!user ? { points, ...ancillary } : ancillary
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ancillary.id}
|
||||
onClick={() =>
|
||||
handleCardClick({ description, points, ...ancillary })
|
||||
}
|
||||
>
|
||||
<AncillaryCard key={ancillary.id} ancillary={ancillaryData} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={styles.mobileAncillaries}>
|
||||
<Carousel>
|
||||
<Carousel.Content className={styles.carouselContainer}>
|
||||
{allAncillaries.map(({ description, ...ancillary }) => (
|
||||
<Carousel.Item key={ancillary.id}>
|
||||
<AncillaryCard key={ancillary.id} ancillary={ancillary} />
|
||||
</Carousel.Item>
|
||||
))}
|
||||
{allAncillaries.map(({ description, points, ...ancillary }) => {
|
||||
const ancillaryData = !!user
|
||||
? { points, ...ancillary }
|
||||
: ancillary
|
||||
return (
|
||||
<Carousel.Item
|
||||
key={ancillary.id}
|
||||
onClick={() =>
|
||||
handleCardClick({ description, points, ...ancillary })
|
||||
}
|
||||
>
|
||||
<AncillaryCard key={ancillary.id} ancillary={ancillaryData} />
|
||||
</Carousel.Item>
|
||||
)
|
||||
})}
|
||||
</Carousel.Content>
|
||||
<Carousel.Previous className={styles.navigationButton} />
|
||||
<Carousel.Next className={styles.navigationButton} />
|
||||
<Carousel.Dots />
|
||||
</Carousel>
|
||||
</div>
|
||||
|
||||
<AddAncillaryFlowModal
|
||||
user={user}
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
booking={booking}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
11
components/HotelReservation/MyStay/Ancillaries/utils.ts
Normal file
11
components/HotelReservation/MyStay/Ancillaries/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
export const generateDeliveryOptions = (checkInDate: Date) => {
|
||||
const start = dt(checkInDate).startOf("day")
|
||||
const timeSlots = ["16:00-17:00", "17:00-18:00", "18:00-19:00", "19:00-20:00"]
|
||||
|
||||
return timeSlots.map((slot) => ({
|
||||
label: `${start.format("YYYY-MM-DD")} ${slot}`,
|
||||
value: `${start.format("YYYY-MM-DD")} ${slot}`,
|
||||
}))
|
||||
}
|
||||
@@ -64,7 +64,11 @@ export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
<ReferenceCard booking={booking} hotel={hotel} />
|
||||
</div>
|
||||
{booking.showAncillaries && (
|
||||
<Ancillaries ancillaries={ancillaryPackages} />
|
||||
<Ancillaries
|
||||
ancillaries={ancillaryPackages}
|
||||
booking={booking}
|
||||
user={user}
|
||||
/>
|
||||
)}
|
||||
<Room booking={booking} room={room} hotel={hotel} user={user} />
|
||||
<BookingSummary booking={booking} hotel={hotel} />
|
||||
|
||||
@@ -11,7 +11,9 @@ export default function Select({
|
||||
label,
|
||||
disabled,
|
||||
name,
|
||||
isNestedInModal = false,
|
||||
registerOptions = {},
|
||||
defaultSelectedKey,
|
||||
}: SelectProps) {
|
||||
const { control } = useFormContext()
|
||||
const { field } = useController({
|
||||
@@ -23,7 +25,7 @@ export default function Select({
|
||||
return (
|
||||
<ReactAriaSelect
|
||||
className={className}
|
||||
defaultSelectedKey={field.value}
|
||||
defaultSelectedKey={defaultSelectedKey || field.value}
|
||||
disabled={disabled || field.disabled}
|
||||
items={items}
|
||||
label={label}
|
||||
@@ -33,6 +35,7 @@ export default function Select({
|
||||
onSelect={field.onChange}
|
||||
value={field.value}
|
||||
data-testid={name}
|
||||
isNestedInModal={isNestedInModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Adults": "voksne",
|
||||
"Age": "Alder",
|
||||
"Airport": "Lufthavn",
|
||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle tillæg leveres på samme tid. Ændringer i leveringstider vil påvirke tidligere tillæg.",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.",
|
||||
"Allergy-friendly room": "Allergirum",
|
||||
"Already a friend?": "Allerede en ven?",
|
||||
@@ -60,6 +61,7 @@
|
||||
"Attractions": "Attraktioner",
|
||||
"Average price per night": "gennemsnitspris pr. nat",
|
||||
"Away from elevator": "Væk fra elevator",
|
||||
"Back": "Tilbage",
|
||||
"Back to scandichotels.com": "Tilbage til scandichotels.com",
|
||||
"Back to top": "Tilbage til top",
|
||||
"Bar": "Bar",
|
||||
@@ -146,6 +148,7 @@
|
||||
"Complete booking": "Fuldfør bookingen",
|
||||
"Complete booking & go to payment": "Udfyld booking & gå til betaling",
|
||||
"Complete the booking": "Fuldfør bookingen",
|
||||
"Confirm": "Bekræft",
|
||||
"Confirm cancellation": "Bekræft annullerering",
|
||||
"Contact information": "Kontaktoplysninger",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
@@ -168,6 +171,8 @@
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Dag",
|
||||
"Delivered at:": "Leveret til:",
|
||||
"Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellem {deliveryTime}. Betaling vil ske ved check-in.",
|
||||
"Description": "Beskrivelse",
|
||||
"Destination": "Destination",
|
||||
"Destinations in {country}": "Destinationer i {country}",
|
||||
@@ -299,6 +304,7 @@
|
||||
"Indoor pool": "Indendørs pool",
|
||||
"Indoor windows and excellent lighting": "Indoor windows and excellent lighting",
|
||||
"Indoor windows facing the hotel": "Indoor windows facing the hotel",
|
||||
"Insufficient points": "Utilstrækkelige point",
|
||||
"Invalid booking code": "Ugyldig reservationskode",
|
||||
"Is there anything else you would like us to know before your arrival?": "Er der andet, du gerne vil have os til at vide, før din ankomst?",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.",
|
||||
@@ -433,6 +439,8 @@
|
||||
"Open my pages menu": "Åbn mine sider menuen",
|
||||
"Open {amount, plural, one {gift} other {gifts}}": "Åbne {amount, plural, one {gave} other {gaver}}",
|
||||
"Opening hours": "Åbningstider",
|
||||
"Optional": "Valgfri",
|
||||
"Other requests": "Andre ønsker",
|
||||
"Outdoor": "Udendørs",
|
||||
"Outdoor pool": "Udendørs pool",
|
||||
"Overview": "Oversigt",
|
||||
@@ -444,6 +452,8 @@
|
||||
"Password": "Adgangskode",
|
||||
"Pay later": "Betal senere",
|
||||
"Pay now": "Betal nu",
|
||||
"Pay with card": "Betal med kort",
|
||||
"Pay with points": "Betal med point",
|
||||
"Payment": "Betaling",
|
||||
"Payment Guarantee": "Garanti betaling",
|
||||
"Payment details": "Payment details",
|
||||
@@ -451,6 +461,7 @@
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
"Payment status": "Payment status",
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.": "Betaling vil ske ved check-in. Kortet vil kun blive brugt til at garantere tillægget i tilfælde af en no-show.",
|
||||
"Per night from": "Per nat fra",
|
||||
"Pet room": "Kæledyrsrum",
|
||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kæledyrsrum har en ekstra gebyr på 20 EUR per ophold",
|
||||
@@ -510,6 +521,7 @@
|
||||
"Reservation No. {reservationNumber}": "Reservation No. {reservationNumber}",
|
||||
"Reservation number {value}": "Reservation number {value}",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Reserve with Card": "Reserver med kort",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Gentag den nye adgangskode",
|
||||
@@ -555,6 +567,7 @@
|
||||
"Select hotel": "Vælg hotel",
|
||||
"Select language": "Vælg sprog",
|
||||
"Select payment method": "Vælg betalingsmetode",
|
||||
"Select quantity": "Vælg antal",
|
||||
"Select room": "Zimmer auswählen",
|
||||
"Select your language": "Vælg dit sprog",
|
||||
"Shopping": "Shopping",
|
||||
@@ -572,6 +585,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
||||
"Something went wrong!": "Noget gik galt!",
|
||||
"Something went wrong. {ancillary} could not be added to your booking!": "Noget gik galt. {ancillary} kunne ikke tilføjes til din booking!",
|
||||
"Sort by": "Sorter efter",
|
||||
"Special requests": "Specielle ønsker",
|
||||
"Spice things up": "Krydre tingene",
|
||||
@@ -740,6 +754,7 @@
|
||||
"{amount} out of {total}": "{amount} ud af {total}",
|
||||
"{amount} {amount, plural, one {hotel} other {hotels}}": "{amount} {amount, plural, one {hotel} other {hoteller}}",
|
||||
"{amount}/night per adult": "{amount}/nat per voksen",
|
||||
"{ancillary} added to your booking!": "{ancillary} tilføjet til din booking!",
|
||||
"{card} ending with {cardno}": "{card} slutter med {cardno}",
|
||||
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
|
||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Adults": "Erwachsene",
|
||||
"Age": "Alter",
|
||||
"Airport": "Flughafen",
|
||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle Add-ons werden gleichzeitig geliefert. Änderungen der Lieferzeiten wirken sich auf frühere Add-ons aus.",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.",
|
||||
"Allergy-friendly room": "Allergikerzimmer",
|
||||
"Already a friend?": "Sind wir schon Freunde?",
|
||||
@@ -61,6 +62,7 @@
|
||||
"Attractions": "Attractions",
|
||||
"Average price per night": "Durchschnittspreis pro Nacht",
|
||||
"Away from elevator": "Weg vom Aufzug",
|
||||
"Back": "Zurück",
|
||||
"Back to scandichotels.com": "Zurück zu scandichotels.com",
|
||||
"Back to top": "Zurück zur Spitze",
|
||||
"Bar": "Bar",
|
||||
@@ -147,6 +149,7 @@
|
||||
"Complete booking": "Buchung abschließen",
|
||||
"Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen",
|
||||
"Complete the booking": "Buchung abschließen",
|
||||
"Confirm": "Bestätigen",
|
||||
"Confirm cancellation": "Stornierung bestätigen",
|
||||
"Contact information": "Kontaktinformationen",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
@@ -169,6 +172,8 @@
|
||||
"Date of Birth": "Geburtsdatum",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Tag",
|
||||
"Delivered at": "Geliefert bei:",
|
||||
"Delivery between {deliveryTime}. Payment will be made on check-in": "Lieferung zwischen {deliveryTime}. Die Zahlung erfolgt beim Check-in.",
|
||||
"Description": "Beschreibung",
|
||||
"Destination": "Bestimmungsort",
|
||||
"Destinations in {country}": "Ziele in {country}",
|
||||
@@ -300,6 +305,7 @@
|
||||
"Indoor pool": "Innenpool",
|
||||
"Indoor windows and excellent lighting": "Indoor windows and excellent lighting",
|
||||
"Indoor windows facing the hotel": "Indoor windows facing the hotel",
|
||||
"Insufficient points": "Zu wenig Punkte",
|
||||
"Invalid booking code": "Ungültiger Buchungscode",
|
||||
"Is there anything else you would like us to know before your arrival?": "Gibt es noch etwas, das Sie uns vor Ihrer Ankunft mitteilen möchten?",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.",
|
||||
@@ -435,6 +441,8 @@
|
||||
"Open my pages menu": "Meine Seiten Menü öffnen",
|
||||
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen",
|
||||
"Opening hours": "Öffnungszeiten",
|
||||
"Optional": "Optional",
|
||||
"Other Requests": "Andere Anfragen",
|
||||
"Outdoor": "Im Freien",
|
||||
"Outdoor pool": "Außenpool",
|
||||
"Overview": "Übersicht",
|
||||
@@ -446,6 +454,8 @@
|
||||
"Password": "Passwort",
|
||||
"Pay later": "Später bezahlen",
|
||||
"Pay now": "Jetzt bezahlen",
|
||||
"Pay with Card": "Mit Karte bezahlen",
|
||||
"Pay with points": "Mit Punkten bezahlen",
|
||||
"Payment": "Zahlung",
|
||||
"Payment Guarantee": "Zahlungsgarantie",
|
||||
"Payment details": "Payment details",
|
||||
@@ -453,6 +463,7 @@
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
"Payment status": "Payment status",
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.": "Die Zahlung erfolgt beim Check-in. Die Karte wird nur zur Garantie der Nebenkosten im Falle eines No-Shows verwendet.",
|
||||
"Per night from": "Pro Nacht ab",
|
||||
"Pet room": "Haustierzimmer",
|
||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Haustierzimmer haben einen zusätzlichen Preis von 20 EUR pro Aufenthalt",
|
||||
@@ -512,6 +523,7 @@
|
||||
"Reservation No. {reservationNumber}": "Reservation No. {reservationNumber}",
|
||||
"Reservation number {value}": "Reservation number {value}",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Reserve with Card": "Mit Karte reservieren",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Neues Passwort erneut eingeben",
|
||||
@@ -557,6 +569,7 @@
|
||||
"Select hotel": "Hotel auswählen",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Select payment method": "Zahlungsart auswählen",
|
||||
"Select quantity": "Menge auswählen",
|
||||
"Select room": "Vælg værelse",
|
||||
"Select your language": "Wählen Sie Ihre Sprache",
|
||||
"Shopping": "Einkaufen",
|
||||
@@ -574,6 +587,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
||||
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||
"Something went wrong. {ancillary} could not be added to your booking!": "Etwas ist schief gelaufen. {ancillary} konnte nicht zu Ihrer Buchung hinzugefügt werden!",
|
||||
"Sort by": "Sortieren nach",
|
||||
"Special requests": "Spezielle Anfragen",
|
||||
"Spice things up": "Würzen Sie die Dinge auf",
|
||||
@@ -741,6 +755,7 @@
|
||||
"{amount} out of {total}": "{amount} von {total}",
|
||||
"{amount} {amount, plural, one {hotel} other {hotels}}": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||
"{amount}/night per adult": "{amount}/Nacht pro Erwachsenem",
|
||||
"{ancillary} added to your booking!": "{ancillary} zu Ihrer Buchung hinzugefügt!",
|
||||
"{card} ending with {cardno}": "{card} endet mit {cardno}",
|
||||
"{checkInDate} from {checkInTime}": "{checkInDate} aus {checkInTime}",
|
||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} aus {checkOutTime}",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"Adults": "Adults",
|
||||
"Age": "Age",
|
||||
"Airport": "Airport",
|
||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||
"Allergy-friendly room": "Allergy room",
|
||||
"Already a friend?": "Already a friend?",
|
||||
@@ -148,6 +149,7 @@
|
||||
"Complete booking": "Complete booking",
|
||||
"Complete booking & go to payment": "Complete booking & go to payment",
|
||||
"Complete the booking": "Complete the booking",
|
||||
"Confirm": "Confirm",
|
||||
"Confirm cancellation": "Confirm cancellation",
|
||||
"Contact information": "Contact information",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
@@ -171,6 +173,8 @@
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Day",
|
||||
"Delivered at:": "Delivered at:",
|
||||
"Delivery between {deliveryTime}. Payment will be made on check-in": "Delivery between {deliveryTime}. Payment will be made on check-in.",
|
||||
"Description": "Description",
|
||||
"Destination": "Destination",
|
||||
"Destinations in {country}": "Destinations in {country}",
|
||||
@@ -303,6 +307,7 @@
|
||||
"Indoor pool": "Indoor pool",
|
||||
"Indoor windows and excellent lighting": "Indoor windows and excellent lighting",
|
||||
"Indoor windows facing the hotel": "Indoor windows facing the hotel",
|
||||
"Insufficient points": "Insufficient points",
|
||||
"Invalid booking code": "Invalid booking code",
|
||||
"Is there anything else you would like us to know before your arrival?": "Is there anything else you would like us to know before your arrival?",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.",
|
||||
@@ -438,6 +443,8 @@
|
||||
"Open my pages menu": "Open my pages menu",
|
||||
"Open {amount, plural, one {gift} other {gifts}}": "Open {amount, plural, one {gift} other {gifts}}",
|
||||
"Opening hours": "Opening hours",
|
||||
"Optional": "Optional",
|
||||
"Other Requests": "Other Requests",
|
||||
"Outdoor": "Outdoor",
|
||||
"Outdoor pool": "Outdoor pool",
|
||||
"Overview": "Overview",
|
||||
@@ -449,6 +456,8 @@
|
||||
"Password": "Password",
|
||||
"Pay later": "Pay later",
|
||||
"Pay now": "Pay now",
|
||||
"Pay with Card": "Pay with Card",
|
||||
"Pay with points": "Pay with points",
|
||||
"Payment": "Payment",
|
||||
"Payment Guarantee": "Payment Guarantee",
|
||||
"Payment details": "Payment details",
|
||||
@@ -456,6 +465,7 @@
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
"Payment status": "Payment status",
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.": "Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.",
|
||||
"Per night from": "Per night from",
|
||||
"Pet room": "Pet room",
|
||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Pet-friendly rooms have an additional fee of 20 EUR per stay",
|
||||
@@ -517,6 +527,7 @@
|
||||
"Reservation No. {reservationNumber}": "Reservation No. {reservationNumber}",
|
||||
"Reservation number {value}": "Reservation number {value}",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Reserve with Card": "Reserve with Card",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Retype new password",
|
||||
@@ -563,6 +574,7 @@
|
||||
"Select hotel": "Select hotel",
|
||||
"Select language": "Select language",
|
||||
"Select payment method": "Select payment method",
|
||||
"Select quantity": "Select quantity",
|
||||
"Select room": "Select room",
|
||||
"Select your language": "Select your language",
|
||||
"Shopping": "Shopping",
|
||||
@@ -580,6 +592,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"Something went wrong. {ancillary} could not be added to your booking!": "Something went wrong. {ancillary} could not be added to your booking!",
|
||||
"Sort by": "Sort by",
|
||||
"Special requests": "Special requests",
|
||||
"Spice things up": "Spice things up",
|
||||
@@ -684,6 +697,7 @@
|
||||
"Windows with natural daylight": "Windows with natural daylight",
|
||||
"Year": "Year",
|
||||
"Yes": "Yes",
|
||||
"Yes, I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.": "Yes I accept the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
|
||||
"Yes, close and remove benefit": "Yes, close and remove benefit",
|
||||
"Yes, discard changes": "Yes, discard changes",
|
||||
"Yes, redeem": "Yes, redeem",
|
||||
@@ -746,6 +760,7 @@
|
||||
"{amount} out of {total}": "{amount} out of {total}",
|
||||
"{amount} {amount, plural, one {hotel} other {hotels}}": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||
"{amount}/night per adult": "{amount}/night per adult",
|
||||
"{ancillary} added to your booking!": "{ancillary} added to your booking!",
|
||||
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
||||
"{checkInDate} from {checkInTime}": "{checkInDate} from {checkInTime}",
|
||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} from {checkOutTime}",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Adults": "Aikuista",
|
||||
"Age": "Ikä",
|
||||
"Airport": "Lentokenttä",
|
||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Kaikki lisäosat toimitetaan samanaikaisesti. Toimitusaikojen muutokset vaikuttavat aiempiin lisäosiin.",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.",
|
||||
"Allergy-friendly room": "Allergiahuone",
|
||||
"Already a friend?": "Oletko jo ystävä?",
|
||||
@@ -59,6 +60,7 @@
|
||||
"Attractions": "Nähtävyydet",
|
||||
"Average price per night": "keskihinta per yö",
|
||||
"Away from elevator": "Kaukana hissistä",
|
||||
"Back": "Takaisin",
|
||||
"Back to scandichotels.com": "Takaisin scandichotels.com",
|
||||
"Back to top": "Takaisin ylös",
|
||||
"Bar": "Bar",
|
||||
@@ -146,6 +148,7 @@
|
||||
"Complete booking": "Complete booking",
|
||||
"Complete booking & go to payment": "Täydennä varaus & siirry maksamaan",
|
||||
"Complete the booking": "Täydennä varaus",
|
||||
"Confirm": "Vahvista",
|
||||
"Confirm cancellation": "Vahvista peruutus",
|
||||
"Contact information": "Yhteystiedot",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
@@ -168,6 +171,8 @@
|
||||
"Date of Birth": "Syntymäaika",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Päivä",
|
||||
"Delivered at:": "Toimitettu:",
|
||||
"Delivery between {deliveryTime}. Payment will be made on check-in": "Toimitus välillä {deliveryTime}. Maksu suoritetaan sisäänkirjautumisen yhteydessä.",
|
||||
"Description": "Kuvaus",
|
||||
"Destination": "Kohde",
|
||||
"Destinations in {country}": "Kohteet maassa {country}",
|
||||
@@ -299,6 +304,7 @@
|
||||
"Indoor pool": "Sisäuima-allas",
|
||||
"Indoor windows and excellent lighting": "Indoor windows and excellent lighting",
|
||||
"Indoor windows facing the hotel": "Indoor windows facing the hotel",
|
||||
"Insufficient points": "Riittämättä pisteitä",
|
||||
"Invalid booking code": "Virheellinen varauskoodi",
|
||||
"Is there anything else you would like us to know before your arrival?": "Onko jotain muuta, mitä haluaisit meidän tietävän ennen saapumistasi?",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
|
||||
@@ -434,6 +440,8 @@
|
||||
"Open my pages menu": "Avaa omat sivut -valikko",
|
||||
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}",
|
||||
"Opening hours": "Aukioloajat",
|
||||
"Optional": "Valinnainen",
|
||||
"Other Requests": "Muut pyynnöt",
|
||||
"Outdoor": "Ulkona",
|
||||
"Outdoor pool": "Ulkouima-allas",
|
||||
"Overview": "Yleiskatsaus",
|
||||
@@ -445,6 +453,8 @@
|
||||
"Password": "Salasana",
|
||||
"Pay later": "Maksa myöhemmin",
|
||||
"Pay now": "Maksa nyt",
|
||||
"Pay with Card": "Maksa kortilla",
|
||||
"Pay with points": "Maksa pisteillä",
|
||||
"Payment": "Maksu",
|
||||
"Payment Guarantee": "Varmistusmaksu",
|
||||
"Payment details": "Payment details",
|
||||
@@ -452,6 +462,7 @@
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
"Payment status": "Payment status",
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.": "Maksu suoritetaan sisäänkirjautumisen yhteydessä. Korttia käytetään vain lisäpalvelun varmistamiseen, jos varausmyyntiä ei tapahdu.",
|
||||
"Per night from": "Per yö alkaen",
|
||||
"Pet room": "Lemmikkihuone",
|
||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Lemmikkihuoneen lisäkustannus on 20 EUR per majoitus",
|
||||
@@ -511,6 +522,7 @@
|
||||
"Reservation No. {reservationNumber}": "Reservation No. {reservationNumber}",
|
||||
"Reservation number {value}": "Reservation number {value}",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Reserve with Card": "Varaa kortilla",
|
||||
"Restaurant & Bar": "Ravintola & Baari",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Kirjoita uusi salasana uudelleen",
|
||||
@@ -557,6 +569,7 @@
|
||||
"Select hotel": "Valitse hotelli",
|
||||
"Select language": "Valitse kieli",
|
||||
"Select payment method": "Valitse maksutapa",
|
||||
"Select quantity": "Valitse määrä",
|
||||
"Select room": "Valitse huone",
|
||||
"Select your language": "Valitse kieli",
|
||||
"Shopping": "Ostokset",
|
||||
@@ -574,6 +587,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Something went wrong!": "Jotain meni pieleen!",
|
||||
"Something went wrong. {ancillary} could not be added to your booking!": "Jotain meni pieleen. {ancillary} ei voitu lisätä varaukseesi!",
|
||||
"Sort by": "Lajitteluperuste",
|
||||
"Special requests": "Erityistoiveet",
|
||||
"Spice things up": "Mausta asioita",
|
||||
@@ -741,6 +755,7 @@
|
||||
"{amount} out of {total}": "{amount}/{total}",
|
||||
"{amount} {amount, plural, one {hotel} other {hotels}}": "{amount} {amount, plural, one {hotelli} other {hotellit}}",
|
||||
"{amount}/night per adult": "{amount}/yötä aikuista kohti",
|
||||
"{ancillary} added to your booking!": "{ancillary} lisätty varaukseesi!",
|
||||
"{card} ending with {cardno}": "{card} päättyen {cardno}",
|
||||
"{checkInDate} from {checkInTime}": "{checkInDate} alkaen {checkInTime}",
|
||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Adults": "Voksne",
|
||||
"Age": "Alder",
|
||||
"Airport": "Flyplass",
|
||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alle tilvalg leveres samtidig. Endringer i leveringstidspunktene vil påvirke tidligere tilvalg.",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
|
||||
"Allergy-friendly room": "Allergirom",
|
||||
"Already a friend?": "Allerede Friend?",
|
||||
@@ -59,6 +60,7 @@
|
||||
"Attractions": "Attraksjoner",
|
||||
"Average price per night": "gjennomsnittlig pris per natt",
|
||||
"Away from elevator": "Bort fra heisen",
|
||||
"Back": "Tilbake",
|
||||
"Back to scandichotels.com": "Tilbake til scandichotels.com",
|
||||
"Back to top": "Tilbake til toppen",
|
||||
"Bar": "Bar",
|
||||
@@ -145,6 +147,7 @@
|
||||
"Complete booking": "Fullfør reservasjonen",
|
||||
"Complete booking & go to payment": "Fullfør bestilling & gå til betaling",
|
||||
"Complete the booking": "Fullfør reservasjonen",
|
||||
"Confirm": "Bekreft",
|
||||
"Confirm cancellation": "Bekræft annullerering",
|
||||
"Contact information": "Kontaktinformasjon",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
@@ -167,6 +170,8 @@
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Dag",
|
||||
"Delivered at:": "Delivered at:",
|
||||
"Delivery between {deliveryTime}. Payment will be made on check-in": "Levering mellom {deliveryTime}. Betaling vil skje ved innsjekking.",
|
||||
"Description": "Beskrivelse",
|
||||
"Destination": "Destinasjon",
|
||||
"Destinations in {country}": "Destinasjoner i {country}",
|
||||
@@ -298,6 +303,7 @@
|
||||
"Indoor pool": "Innendørs basseng",
|
||||
"Indoor windows and excellent lighting": "Indoor windows and excellent lighting",
|
||||
"Indoor windows facing the hotel": "Indoor windows facing the hotel",
|
||||
"Insufficient points": "Utilstrekklig poeng",
|
||||
"Invalid booking code": "Ugyldig bookingkode",
|
||||
"Is there anything else you would like us to know before your arrival?": "Er det noe annet du vil at vi skal vite før ankomsten din?",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.",
|
||||
@@ -433,6 +439,8 @@
|
||||
"Open my pages menu": "Åpne mine sider menyen",
|
||||
"Open {amount, plural, one {gift} other {gifts}}": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}",
|
||||
"Opening hours": "Åpningstider",
|
||||
"Optional": "Valgfritt",
|
||||
"Other Requests": "Andre ønsker",
|
||||
"Outdoor": "Utendørs",
|
||||
"Outdoor pool": "Utendørs basseng",
|
||||
"Overview": "Oversikt",
|
||||
@@ -451,6 +459,7 @@
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
"Payment status": "Payment status",
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.": "Betaling vil skje ved innsjekking. Kortet vil kun bli brukt til å garantere tillegget i tilfelle av en no-show.",
|
||||
"Per night from": "Per nat fra",
|
||||
"Pet room": "Kjæledyrsrom",
|
||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Kjæledyrsrom har en tilleggsavgift på 20 EUR per opphold",
|
||||
@@ -510,6 +519,7 @@
|
||||
"Reservation No. {reservationNumber}": "Reservation No. {reservationNumber}",
|
||||
"Reservation number {value}": "Reservation number {value}",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Reserve with Card": "Reserver med kort",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Skriv inn nytt passord på nytt",
|
||||
@@ -555,6 +565,7 @@
|
||||
"Select hotel": "Velg hotell",
|
||||
"Select language": "Velg språk",
|
||||
"Select payment method": "Velg betalingsmetode",
|
||||
"Select quantity": "Velg antall",
|
||||
"Select room": "Velg rom",
|
||||
"Select your language": "Velg språk",
|
||||
"Shopping": "Shopping",
|
||||
@@ -572,6 +583,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
||||
"Something went wrong!": "Noe gikk galt!",
|
||||
"Something went wrong. {ancillary} could not be added to your booking!": "Noe gikk galt. {ancillary} kunne ikke legges til bestillingen din!",
|
||||
"Sort by": "Sorter etter",
|
||||
"Special requests": "Spesielle ønsker",
|
||||
"Spice things up": "Krydre tingene",
|
||||
@@ -739,6 +751,7 @@
|
||||
"{amount} out of {total}": "{amount} av {total}",
|
||||
"{amount} {amount, plural, one {hotel} other {hotels}}": "{amount} {amount, plural, one {hotell} other {hoteller}}",
|
||||
"{amount}/night per adult": "{amount}/natt per voksen",
|
||||
"{ancillary} added to your booking!": "{ancillary} lagt til bestillingen din!",
|
||||
"{card} ending with {cardno}": "{card} slutter med {cardno}",
|
||||
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
|
||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"Adults": "Vuxna",
|
||||
"Age": "Ålder",
|
||||
"Airport": "Flygplats",
|
||||
"All add-ons are delivered at the same time. Changes to delivery times will affect earlier add-ons.": "Alla tillägg levereras samtidigt. Ändringar av leveranstider kommer att påverka tidigare tillägg.",
|
||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.",
|
||||
"Allergy-friendly room": "Allergirum",
|
||||
"Already a friend?": "Är du redan en vän?",
|
||||
@@ -59,6 +60,7 @@
|
||||
"Attractions": "Sevärdheter",
|
||||
"Average price per night": "Snittpris per natt",
|
||||
"Away from elevator": "Bort från hissen",
|
||||
"Back": "Tillbaka",
|
||||
"Back to scandichotels.com": "Tillbaka till scandichotels.com",
|
||||
"Back to top": "Tillbaka till toppen",
|
||||
"Bar": "Bar",
|
||||
@@ -145,6 +147,7 @@
|
||||
"Complete booking": "Slutför bokning",
|
||||
"Complete booking & go to payment": "Fullför bokning & gå till betalning",
|
||||
"Complete the booking": "Slutför bokningen",
|
||||
"Confirm": "Bekräfta",
|
||||
"Confirm cancellation": "Bekräfta avbokning",
|
||||
"Contact information": "Kontaktinformation",
|
||||
"Contact our memberservice": "Contact our memberservice",
|
||||
@@ -167,6 +170,8 @@
|
||||
"Date of Birth": "Födelsedatum",
|
||||
"Date of birth not matching": "Date of birth not matching",
|
||||
"Day": "Dag",
|
||||
"Delivered at:": "Levereras vid:",
|
||||
"Delivery between {deliveryTime}. Payment will be made on check-in": "Leverans mellan {deliveryTime}. Betalning kommer att göras vid incheckning.",
|
||||
"Description": "Beskrivning",
|
||||
"Destination": "Destination",
|
||||
"Destinations in {country}": "Destinationer i {country}",
|
||||
@@ -298,6 +303,7 @@
|
||||
"Indoor pool": "Inomhuspool",
|
||||
"Indoor windows and excellent lighting": "Fönster inomhus och utmärkt belysning",
|
||||
"Indoor windows facing the hotel": "Inomhusfönster mot hotellet",
|
||||
"Insufficient points": "Otillräckliga poäng",
|
||||
"Invalid booking code": "Ogiltig bokningskod",
|
||||
"Is there anything else you would like us to know before your arrival?": "Är det något mer du vill att vi ska veta innan din ankomst?",
|
||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.",
|
||||
@@ -433,6 +439,8 @@
|
||||
"Open my pages menu": "Öppna mina sidor menyn",
|
||||
"Open {amount, plural, one {gift} other {gifts}}": "Öppna {amount, plural, one {gåva} other {gåvor}}",
|
||||
"Opening hours": "Öppettider",
|
||||
"Optional": "Valfritt",
|
||||
"Other Requests": "Övriga önskemål",
|
||||
"Outdoor": "Utomhus",
|
||||
"Outdoor pool": "Utomhuspool",
|
||||
"Overview": "Översikt",
|
||||
@@ -451,6 +459,7 @@
|
||||
"Payment method": "Payment method",
|
||||
"Payment received": "Payment received",
|
||||
"Payment status": "Payment status",
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.": "Betalning kommer att ske vid incheckning. Kortet kommer endast att användas för att garantera tillägget i händelse av no-show.",
|
||||
"Per night from": "Per natt från",
|
||||
"Pet room": "Husdjursrum",
|
||||
"Pet-friendly rooms have an additional fee of 20 EUR per stay": "Husdjursrum har en extra avgift på 20 EUR per vistelse",
|
||||
@@ -510,6 +519,7 @@
|
||||
"Reservation No. {reservationNumber}": "Reservation No. {reservationNumber}",
|
||||
"Reservation number {value}": "Reservation number {value}",
|
||||
"Reservation policy": "Reservation policy",
|
||||
"Reserve with Card": "Reservera med kort",
|
||||
"Restaurant & Bar": "Restaurang & Bar",
|
||||
"Restaurants & Bars": "Restaurants & Bars",
|
||||
"Retype new password": "Upprepa nytt lösenord",
|
||||
@@ -555,6 +565,7 @@
|
||||
"Select hotel": "Välj hotell",
|
||||
"Select language": "Välj språk",
|
||||
"Select payment method": "Välj betalningsmetod",
|
||||
"Select quantity": "Välj antal",
|
||||
"Select room": "Välj rum",
|
||||
"Select your language": "Välj ditt språk",
|
||||
"Shopping": "Shopping",
|
||||
@@ -572,6 +583,7 @@
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
||||
"Something went wrong!": "Något gick fel!",
|
||||
"Something went wrong. {ancillary} could not be added to your booking!": "Något gick fel. {ancillary} kunde inte läggas till i din bokning!",
|
||||
"Sort by": "Sortera efter",
|
||||
"Special requests": "Särskilda önskemål",
|
||||
"Spice things up": "Krydda upp saker och ting",
|
||||
@@ -741,6 +753,7 @@
|
||||
"{amount} out of {total}": "{amount} av {total}",
|
||||
"{amount} {amount, plural, one {hotel} other {hotels}}": "{amount} {amount, plural, one {hotell} other {hotell}}",
|
||||
"{amount}/night per adult": "{amount}/natt per vuxen",
|
||||
"{ancillary} added to your booking!": "{ancillary} har lagts till i din bokning!",
|
||||
"{card} ending with {cardno}": "{card} som slutar på {cardno}",
|
||||
"{checkInDate} from {checkInTime}": "{checkInDate} från {checkInTime}",
|
||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} från {checkOutTime}",
|
||||
|
||||
@@ -65,6 +65,9 @@ export namespace endpoints {
|
||||
export function priceChange(confirmationNumber: string) {
|
||||
return `${bookings}/${confirmationNumber}/priceChange`
|
||||
}
|
||||
export function packages(confirmationNumber: string) {
|
||||
return `${bookings}/${confirmationNumber}/packages`
|
||||
}
|
||||
|
||||
export const enum Stays {
|
||||
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,
|
||||
|
||||
@@ -85,6 +85,20 @@ export const createBookingInput = z.object({
|
||||
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
||||
})
|
||||
|
||||
export const addPackageInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
ancillaryComment: z.string(),
|
||||
ancillaryDeliveryTime: z.string().optional(),
|
||||
packages: z.array(
|
||||
z.object({
|
||||
code: z.string(),
|
||||
quantity: z.number(),
|
||||
comment: z.string().optional(),
|
||||
})
|
||||
),
|
||||
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
||||
})
|
||||
|
||||
export const priceChangeInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getMembership } from "@/utils/user"
|
||||
|
||||
import {
|
||||
cancelBookingInput,
|
||||
addPackageInput,
|
||||
createBookingInput,
|
||||
priceChangeInput,
|
||||
} from "./input"
|
||||
@@ -39,6 +40,14 @@ const cancelBookingFailCounter = meter.createCounter(
|
||||
"trpc.bookings.cancel-fail"
|
||||
)
|
||||
|
||||
const addPackageCounter = meter.createCounter("trpc.bookings.add-package")
|
||||
const addPackageSuccessCounter = meter.createCounter(
|
||||
"trpc.bookings.add-package-success"
|
||||
)
|
||||
const addPackageFailCounter = meter.createCounter(
|
||||
"trpc.bookings.add-package-fail"
|
||||
)
|
||||
|
||||
async function getMembershipNumber(
|
||||
session: Session | null
|
||||
): Promise<string | undefined> {
|
||||
@@ -304,6 +313,70 @@ export const bookingMutationRouter = router({
|
||||
})
|
||||
)
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
packages: safeProtectedServiceProcedure
|
||||
.input(addPackageInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { confirmationNumber, ...body } = input
|
||||
|
||||
addPackageCounter.add(1, { confirmationNumber })
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
|
||||
const apiResponse = await api.post(
|
||||
api.endpoints.v1.Booking.packages(confirmationNumber),
|
||||
{
|
||||
headers,
|
||||
body: body,
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
addPackageFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.addPackage error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
addPackageFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
})
|
||||
console.error(
|
||||
"api.booking.addPackage validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
addPackageSuccessCounter.add(1, { confirmationNumber })
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ export const createBookingSchema = z
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
reservationStatus: z.string(),
|
||||
paymentUrl: z.string().nullable(),
|
||||
paymentUrl: z.string().nullable().optional(),
|
||||
rooms: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -81,6 +81,7 @@ const guestSchema = z.object({
|
||||
const packageSchema = z
|
||||
.object({
|
||||
description: z.string().nullable().default(""),
|
||||
type: z.string().nullable().default(""),
|
||||
code: z.string().nullable().default(""),
|
||||
price: z.object({
|
||||
unit: z.number().int().nullable(),
|
||||
@@ -94,6 +95,7 @@ const packageSchema = z
|
||||
.transform((packageData) => ({
|
||||
description: packageData.description,
|
||||
code: packageData.code,
|
||||
type: packageData.type,
|
||||
currency: packageData.price.currency,
|
||||
points: packageData.price.points,
|
||||
totalPrice: packageData.price.totalPrice ?? 0,
|
||||
@@ -195,7 +197,7 @@ export const bookingConfirmationSchema = z
|
||||
createDateTime: z.date({ coerce: true }),
|
||||
currencyCode: z.string(),
|
||||
guest: guestSchema,
|
||||
isGuaranteedForLateArrival: z.boolean(),
|
||||
isGuaranteedForLateArrival: z.boolean().optional(),
|
||||
linkedReservations: z.array(linkedReservationsSchema).default([]),
|
||||
hotelId: z.string(),
|
||||
packages: z.array(packageSchema).default([]),
|
||||
|
||||
@@ -371,6 +371,8 @@ export const ancillaryPackagesSchema = z
|
||||
currency: item.variants.ancillary.price.currency,
|
||||
},
|
||||
points: item.variants.ancillaryLoyalty?.points,
|
||||
loyaltyCode: item.variants.ancillaryLoyalty?.code,
|
||||
requiresDeliveryTime: item.requiresDeliveryTime,
|
||||
})),
|
||||
}))
|
||||
.filter((ancillary) => ancillary.ancillaryContent.length > 0)
|
||||
|
||||
@@ -29,12 +29,15 @@ export const ancillaryContentSchema = z.object({
|
||||
status: z.string(),
|
||||
id: z.string(),
|
||||
variants: z.object({
|
||||
ancillary: z.object({ price: packagePriceSchema }),
|
||||
ancillaryLoyalty: z.object({ points: z.number() }).optional(),
|
||||
ancillary: z.object({ id: z.string(), price: packagePriceSchema }),
|
||||
ancillaryLoyalty: z
|
||||
.object({ points: z.number(), code: z.string() })
|
||||
.optional(),
|
||||
}),
|
||||
title: z.string(),
|
||||
descriptions: z.object({ html: z.string() }),
|
||||
images: z.array(z.object({ imageSizes: imageSizesSchema })),
|
||||
requiresDeliveryTime: z.boolean(),
|
||||
})
|
||||
|
||||
export const packageSchema = z.object({
|
||||
|
||||
42
stores/my-stay/add-ancillary-flow.ts
Normal file
42
stores/my-stay/add-ancillary-flow.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
import type { Ancillary } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
interface AddAncillaryState {
|
||||
step: number
|
||||
totalSteps: number
|
||||
nextStep: () => void
|
||||
prevStep: () => void
|
||||
resetStore: () => void
|
||||
selectedAncillary: Ancillary["ancillaryContent"][number] | null
|
||||
setSelectedAncillary: (
|
||||
ancillary: Ancillary["ancillaryContent"][number]
|
||||
) => void
|
||||
confirmationNumber: string
|
||||
setConfirmationNumber: (confirmationNumber: string) => void
|
||||
openedFrom: "list" | "grid" | null
|
||||
setOpenedFrom: (source: "list" | "grid") => void
|
||||
isGridOpen: boolean
|
||||
setGridIsOpen: (isOpen: boolean) => void
|
||||
}
|
||||
|
||||
export const useAddAncillaryStore = create<AddAncillaryState>((set) => ({
|
||||
step: 1,
|
||||
totalSteps: 3,
|
||||
nextStep: () =>
|
||||
set((state) =>
|
||||
state.step < state.totalSteps ? { step: state.step + 1 } : {}
|
||||
),
|
||||
prevStep: () =>
|
||||
set((state) => (state.step > 1 ? { step: state.step - 1 } : {})),
|
||||
resetStore: () => set({ step: 1 }),
|
||||
selectedAncillary: null,
|
||||
setSelectedAncillary: (ancillary) => set({ selectedAncillary: ancillary }),
|
||||
confirmationNumber: "",
|
||||
setConfirmationNumber: (confirmationNumber) =>
|
||||
set({ confirmationNumber: confirmationNumber }),
|
||||
openedFrom: null,
|
||||
setOpenedFrom: (source) => set({ openedFrom: source }),
|
||||
isGridOpen: false,
|
||||
setGridIsOpen: (isOpen) => set({ isGridOpen: isOpen }),
|
||||
}))
|
||||
@@ -19,3 +19,9 @@ export interface BreakfastChoiceCardProps extends AncillaryCardProps {
|
||||
id?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface AncillaryChoiceCardProps extends AncillaryCardProps {
|
||||
name: string
|
||||
id?: string
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { User } from "@/types/user"
|
||||
import type { ancillaryPackagesSchema } from "@/server/routers/hotels/output"
|
||||
|
||||
export type Ancillaries = z.output<typeof ancillaryPackagesSchema>
|
||||
export type Ancillary = Ancillaries[number]
|
||||
|
||||
export interface AncillariesProps {
|
||||
export interface AncillariesProps extends Pick<BookingConfirmation, "booking"> {
|
||||
ancillaries: Ancillaries | null
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export interface AncillaryProps {
|
||||
@@ -17,3 +19,28 @@ export interface AncillaryProps {
|
||||
export interface MyStayProps extends BookingConfirmation {
|
||||
ancillaries: Ancillaries | null
|
||||
}
|
||||
|
||||
export interface AncillaryGridModalProps {
|
||||
ancillaries: Ancillaries
|
||||
selectedCategory: string | null
|
||||
setSelectedCategory: (category: string) => void
|
||||
handleCardClick: (ancillary: Ancillary["ancillaryContent"][number]) => void
|
||||
}
|
||||
|
||||
export interface AddAncillaryFlowModalProps
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export interface DeliveryMethodStepProps {
|
||||
deliveryTimeOptions: {
|
||||
label: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface SelectQuantityStepProps {
|
||||
user: User | null
|
||||
}
|
||||
|
||||
@@ -188,3 +188,4 @@ export type TrackingPosition =
|
||||
| "hamburger menu"
|
||||
| "join scandic friends sidebar"
|
||||
| "enter details"
|
||||
| "my stay"
|
||||
|
||||
Reference in New Issue
Block a user