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,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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user