/* eslint-disable formatjs/no-literal-string-in-jsx */ /* TODO remove disable and add i18n */ /* TODO add analytics */ import { zodResolver } from "@hookform/resolvers/zod" import { cx } from "class-variance-authority" import { useState } from "react" import { FormProvider, useForm, useWatch } from "react-hook-form" import { useIntl } from "react-intl" import z from "zod" import { dt } from "@scandic-hotels/common/dt" import { Button } from "@scandic-hotels/design-system/Button" import { FormInput } from "@scandic-hotels/design-system/Form/FormInput" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner" import { Typography } from "@scandic-hotels/design-system/Typography" import { trpc } from "@scandic-hotels/trpc/client" import useLang from "@/hooks/useLang" import styles from "./claimPoints.module.css" type PointClaimBookingInfo = { from: string to: string city: string hotel: string } export function ClaimPointsWizard({ onSuccess, onClose, }: { onSuccess: () => void onClose: () => void }) { const [state, setState] = useState< "initial" | "loading" | "invalid" | "form" >("initial") const [bookingDetails, setBookingDetails] = useState(null) const { data, isLoading } = trpc.user.getSafely.useQuery() if (state === "invalid") { return } if (state === "form") { if (isLoading) { return null } return ( ) } const handleBookingNumberEvent = (event: BookingNumberEvent) => { switch (event.type) { case "submit": setState("loading") break case "error": setState("initial") break case "invalid": setState("invalid") break case "success": setBookingDetails(event.data) setState("form") break } } return (
{state === "loading" && (
)}

Claim points with booking number

Enter a valid booking number to load booking details automatically.

Claim points without booking number

You need to add booking details in a form.

) } type BookingNumberFormData = { bookingNumber: string } type BookingNumberEvent = | { type: "submit" } | { type: "success"; data: PointClaimBookingInfo } | { type: "error" } | { type: "invalid" } function BookingNumberInput({ onEvent, }: { onEvent: (event: BookingNumberEvent) => void }) { const lang = useLang() const form = useForm({ resolver: zodResolver( z.object({ bookingNumber: z .string() // TODO Check UX for validation as different environments have different lengths .min(9, { message: "Booking number must be 10 digits" }) .max(10, { message: "Booking number must be 10 digits" }), }) ), defaultValues: { bookingNumber: "", }, }) const confirmationNumber = useWatch({ name: "bookingNumber", control: form.control, }) const { refetch, isFetching } = trpc.booking.findBookingForCurrentUser.useQuery( { confirmationNumber, lang, }, { enabled: false } ) const handleSubmit = async () => { onEvent({ type: "submit" }) const result = await refetch() if (!result.data) { onEvent({ type: "error" }) form.setError("bookingNumber", { type: "manual", message: "We could not find a booking with this number registered in your name.", }) return } const data = result.data // TODO validate if this should be check out or check in date const checkOutDate = dt(data.booking.checkOutDate) const sixMonthsAgo = dt().subtract(6, "months") if (checkOutDate.isBefore(sixMonthsAgo, "day")) { onEvent({ type: "invalid" }) return } onEvent({ type: "success", data: { from: data.booking.checkInDate, to: data.booking.checkOutDate, city: data.hotel.cityName, hotel: data.hotel.name, }, }) } return (
} description="Enter your 10-digit booking number" maxLength={10} showClearContentIcon disabled={isFetching} autoFocus autoComplete="off" onChange={(e) => { const value = e.target.value if (value.length !== 10) return form.handleSubmit(handleSubmit)() }} />
) } function InvalidBooking({ onClose }: { onClose: () => void }) { return (

We can’t add these points to your account as it has been longer than 6 months since your stay.

) } type PointClaimUserInfo = { firstName: string lastName: string email: string phone: string } function ClaimPointsForm({ onSuccess, initialData, }: { onSuccess: () => void initialData: Partial | null }) { const form = useForm({ resolver: zodResolver( z.object({ from: z.string().min(1, { message: "Arrival date is required" }), to: z.string().min(1, { message: "Departure date is required" }), city: z.string().min(1, { message: "City is required" }), hotel: z.string().min(1, { message: "Hotel is required" }), firstName: z.string().min(1, { message: "First name is required" }), lastName: z.string().min(1, { message: "Last name is required" }), email: z .string() .email("Enter a valid email") .min(1, { message: "Email is required" }), phone: z.string().min(1, { message: "Phone is required" }), }) ), defaultValues: { from: initialData?.from || "", to: initialData?.to || "", city: initialData?.city || "", hotel: initialData?.hotel || "", firstName: initialData?.firstName || "", lastName: initialData?.lastName || "", email: initialData?.email || "", phone: initialData?.phone || "", }, mode: "all", }) const { mutate, isPending } = trpc.user.claimPoints.useMutation({ onSuccess, }) const autoFocusField = getAutoFocus(initialData) return (
mutate(data))} >
{!initialData?.firstName && ( )} {!initialData?.lastName && ( )} {!initialData?.email && ( )} {!initialData?.phone && ( )} } autoFocus={autoFocusField === "from"} readOnly={isPending} registerOptions={{ required: true }} /> } readOnly={isPending} registerOptions={{ required: true }} />
) } function getAutoFocus(userInfo: Partial | null) { if (!userInfo?.firstName) { return "firstName" } if (!userInfo?.lastName) { return "lastName" } if (!userInfo?.email) { return "email" } if (!userInfo?.phone) { return "phone" } return "from" } function Divider() { const intl = useIntl() return (
{intl.formatMessage({ id: "common.or", defaultMessage: "or", })}
) }