Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- "use client";
- import { useState } from "react";
- import { Button, Checkbox, Input, ListItem, Text, Title } from "@/components";
- import { useKlubStore } from "@/providers/klub-store-provider";
- import { getClientReqConfig, kAxios, notifyError } from "@/utils/axios";
- import { getCheckout } from "@/utils/endpoints/checkout";
- import { validateEmail, validatePhone } from "@/utils/form-fields-validators";
- import { CalendarX, CircleNotch } from "@phosphor-icons/react";
- import { useMutation, useQuery } from "@tanstack/react-query";
- import { useParams, useRouter } from "next/navigation";
- import { useEffect, useTransition } from "react";
- import { FormProvider, useFieldArray, useForm } from "react-hook-form";
- import { twMerge } from "tailwind-merge";
- import { useMe } from "@/components/hooks/use-me";
- import type { HotelDetail } from "@/utils/types/hotel";
- import { format, parseISO } from "date-fns";
- import { useLoyaltyPoints } from "@/components/hooks/use-loyalty-points";
- import { getNightsAmount } from "@/utils/dates";
- import { formatMoney } from "@/utils/text";
- import type { CheckoutSession } from "@/utils/types/checkout";
- import { useMixpanel } from "@/components/hooks/use-mixpanel";
- import { Voucher } from "@/components/molecules/voucher/voucher";
- type FormGuest = {
- firstName: string;
- lastName: string;
- email: string;
- phone: string;
- };
- type FormProps = {
- guestsRequests: CheckoutSession["guestsRequests"];
- guests: FormGuest[];
- phone: string;
- useKlubPoints: boolean;
- };
- type Tax = {
- included: boolean;
- currency: string;
- amount: number;
- description: string;
- };
- export const CheckoutForm = () => {
- const [isTransitioning, startTransition] = useTransition();
- const { data: me } = useMe();
- const { checkoutId } = useParams<{ checkoutId: string }>();
- const { adults, children } = useKlubStore((state) => state.search);
- const { isPartner, token } = useKlubStore((state) => state.auth);
- const { data: loyalty } = useLoyaltyPoints();
- const [discountPercentage, setCouponDiscountPercentage] = useState(0);
- const guests = adults + children;
- const router = useRouter();
- const methods = useForm<FormProps>({
- defaultValues: {
- useKlubPoints: false,
- guestsRequests: {
- airportDrop: false,
- additionalBed: false,
- earlyCheckIn: false,
- lateCheckout: false,
- remarks: "",
- },
- guests: [],
- phone: "",
- },
- mode: "onSubmit",
- reValidateMode: "onChange",
- });
- const { track } = useMixpanel();
- const { data: checkoutData, isFetching } = useQuery({
- queryKey: ["checkout", checkoutId],
- queryFn: () => getCheckout(checkoutId),
- });
- const useLoyaltyPointsWatch = methods.watch("useKlubPoints");
- const useLoyaltyPointsTouched =
- !!methods.formState.touchedFields.useKlubPoints;
- const hotelId = checkoutData?.prebookResponsePayload.hotelId;
- const { data: hotelData } = useQuery({
- enabled: !!hotelId,
- queryKey: ["hotel", hotelId],
- queryFn: async () =>
- (await kAxios.get<HotelDetail>(`/hotels/${hotelId}`)).data,
- });
- const isKlubPointSelected = methods.watch("useKlubPoints");
- const paymentSessionUrl = checkoutData?.paymentSessionUrl;
- useEffect(() => {
- if (useLoyaltyPointsTouched) {
- track("hotel_checkout.use_loyalty.clicked", {
- useKlubPoints: useLoyaltyPointsWatch ? "yes" : "no",
- });
- }
- }, [useLoyaltyPointsWatch, useLoyaltyPointsTouched, track]);
- useEffect(() => {
- if (!me) return;
- const [firstName, lastName] = me.name.split(" ");
- methods.setValue(
- "guests",
- Array.from<number>({ length: 1 }).map((_: number, index: number) =>
- index === 0
- ? {
- firstName,
- lastName,
- email: me.email,
- phone: "",
- }
- : {
- firstName: "",
- lastName: "",
- email: "",
- phone: "",
- },
- ),
- );
- }, [guests, methods, me]);
- useEffect(() => {
- if (paymentSessionUrl) {
- router.push(paymentSessionUrl);
- }
- }, [paymentSessionUrl, router]);
- const mutation = useMutation({
- mutationFn: (payload: {
- guestsDetails: CheckoutSession["guestsDetails"];
- holderDetails: CheckoutSession["holderDetails"];
- hotelDetails: CheckoutSession["hotelDetails"];
- guestsRequests: CheckoutSession["guestsRequests"];
- klubPointsAmount: CheckoutSession["klubPointsAmount"];
- discountCoupon: number;
- }) => {
- return kAxios.post<{ url: string }>(
- `/payments/session/${checkoutId}`,
- payload,
- getClientReqConfig({ withAuth: true }),
- );
- },
- onSuccess: (data) => {
- startTransition(() => {
- router.push(data.data.url);
- });
- },
- onError: notifyError,
- });
- const errors = methods.formState.errors;
- const onSubmit = async (data: FormProps) => {
- const [firstName, lastName] = me
- ? me.name.split(" ")
- : [data.guests[0].firstName, data.guests[0].lastName];
- track("hotel_checkout.go_to_payment.clicked", {
- guests: data.guests.length,
- hotelName: hotelData?.name || "",
- useKlubPoints: data.useKlubPoints ? "yes" : "no",
- });
- mutation.mutate({
- guestsDetails: (data.guests || []).map((guest) => ({
- firstName: guest.firstName,
- lastName: guest.lastName,
- email: guest.email,
- occupancyNumber: 1,
- })),
- hotelDetails: {
- name: hotelData?.name || "",
- address: hotelData?.address || "",
- city: hotelData?.city || "",
- image: hotelData?.main_photo || "",
- },
- holderDetails: {
- firstName: firstName,
- lastName: lastName,
- email: me ? me.email : data.guests[0].email,
- phone: data.phone,
- },
- guestsRequests: data.guestsRequests,
- klubPointsAmount: data.useKlubPoints ? loyalty?.klubPoints : 0,
- discountCoupon: couponDiscount,
- });
- };
- if (isFetching) {
- return (
- <div className="flex justify-center items-center min-h-[600px]">
- <div className="animate-spin text-primary">
- <CircleNotch size={48} />
- </div>
- </div>
- );
- }
- if (!checkoutData || !hotelData) {
- return null;
- }
- const nrOfNights = getNightsAmount(
- checkoutData.checkin,
- checkoutData.checkout,
- );
- const prebook = checkoutData.prebookResponsePayload;
- const couponDiscount = parseFloat(
- (prebook.price * discountPercentage).toFixed(2),
- );
- const totalAmount = Math.max(
- 0,
- isKlubPointSelected && loyalty?.equivalentAmount
- ? prebook.price - loyalty.equivalentAmount - couponDiscount
- : prebook.price - couponDiscount,
- );
- const rate = prebook?.roomTypes?.[0]?.rates?.[0];
- const isRefundable = rate?.cancellationPolicies?.refundableTag === "RFN";
- const cancelTime =
- rate?.cancellationPolicies?.cancelPolicyInfos?.[0]?.cancelTime;
- const includedTaxesAndFees = rate?.retailRate?.taxesAndFees?.filter(
- (tax: Tax) => !!tax.included,
- ) as Tax[];
- const exludedTaxesAndFees = rate?.retailRate?.taxesAndFees?.filter(
- (tax: Tax) => !tax.included,
- ) as Tax[];
- return (
- <FormProvider {...methods}>
- <form onSubmit={methods.handleSubmit(onSubmit)}>
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
- <div className="col-span-1 lg:col-span-2 grid gap-10 pt-6 relative">
- <section>
- <div>
- <div>{hotelData.name}</div>
- <div className="text-12-regular text-neutral-50">
- {hotelData.address}, {hotelData.city}
- </div>
- </div>
- <div className="rounded-t flex flex-col md:flex-row border border-neutral-20 mt-3">
- <div className="flex-1 flex flex-col md:flex-row gap-4 p-4">
- <div
- className="w-full md:w-[140px] h-[140px] md:min-w-[140px] rounded bg-center bg-cover"
- style={{
- backgroundImage: `url(${hotelData.main_photo})`,
- }}
- />
- <div>
- <div className="grid grid-cols-2 gap-6">
- <div>
- <div className="text-12-semibold">Check in</div>
- <div className="text-16-semibold mt-2">
- {format(parseISO(checkoutData.checkin), "dd MMM")}
- </div>
- <div className="text-12-semibold text-neutral-50">
- {hotelData.checkinCheckoutTimes.checkin}
- </div>
- </div>
- <div>
- <div className="text-12-semibold">Check out</div>
- <div className="text-16-semibold mt-2">
- {format(parseISO(checkoutData.checkout), "dd MMM")}
- </div>
- <div className="text-12-semibold text-neutral-90">
- {hotelData.checkinCheckoutTimes.checkout}
- </div>
- </div>
- </div>
- <div className="text-12-semibold mt-4 mb-2">Room</div>
- <div className="text-16-semibold">
- {adults + children} Guests
- </div>
- <div className="text-12-semibold text-neutral-50">
- {rate?.name}
- </div>
- </div>
- </div>
- <div className="flex-1 md:max-w-[275px] p-4 border-t md:border-t-0 md:border-l border-neutral-20">
- <CalendarX size={24} className="text-red-400" />
- <div className="text-16-semibold">Cancellation policy</div>
- <div className="text-2-semibold mt-5">
- {isRefundable ? "Free cancellation" : "Non-refundable"}
- </div>
- {isRefundable && (
- <div className="text-12-regular text-neutral-50 mt-2">
- Cancel before:{" "}
- {format(cancelTime, "dd MMM yyyy 'at' HH:mm aaaa")}
- </div>
- )}
- </div>
- </div>
- {!isPartner ||
- (token && (
- <div className="rounded-b border-x border-b border-neutral-20 bg-input p-4 ">
- <Checkbox
- disabled={loyalty?.klubPoints === 0}
- className="bg-white text-neutral-100"
- label={
- loyalty?.klubPoints ? (
- <>
- Use Klub Points:{" "}
- <span className="text-primary">
- {loyalty.klubPoints}
- </span>
- </>
- ) : (
- <>You have 0 Klub Points</>
- )
- }
- {...methods.register("useKlubPoints")}
- />
- </div>
- ))}
- </section>
- <section id="guests">
- <Title tag="h3" className="mb-6 text-24-semibold">
- Guests Details
- </Title>
- <div className="flex flex-col gap-6">
- <div
- className={twMerge(
- "grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4",
- "border border-neutral-20 rounded p-4",
- )}
- >
- <Input
- {...methods.register(`guests.${0}.firstName`, {
- required: "This field is required",
- })}
- variant="secondary"
- placeholder="First name"
- error={errors.guests?.[0]?.firstName?.message}
- />
- <Input
- {...methods.register(`guests.${0}.lastName`, {
- required: "This field is required",
- })}
- placeholder="Last name"
- variant="secondary"
- error={errors.guests?.[0]?.lastName?.message}
- />
- <div className="col-span-1 md:col-span-2">
- <Text tag="span" className="text-xs text-neutral-50">
- Booking details will be shared over this phone number and
- email address
- </Text>
- </div>
- <Input
- {...methods.register(`guests.${0}.email`, {
- required: "This field is required",
- validate: validateEmail,
- })}
- variant="secondary"
- placeholder="Email"
- error={errors.guests?.[0]?.email?.message}
- />
- <Input
- {...methods.register(`phone`, {
- required: "This field is required",
- validate: validatePhone,
- })}
- variant="secondary"
- error={errors.guests?.[0]?.phone?.message}
- placeholder="Mobile number"
- />
- </div>
- </div>
- </section>
- <section id="additional-requests">
- <Title tag="h3" className="mb-6 text-24-semibold">
- Additional Requests
- </Title>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div>
- <Checkbox
- label="Airport drop"
- {...methods.register("guestsRequests.airportDrop")}
- />
- </div>
- <div>
- <Checkbox
- label="Additional bed"
- {...methods.register("guestsRequests.additionalBed")}
- />
- </div>
- <div>
- <Checkbox
- label="Early checkin"
- {...methods.register("guestsRequests.earlyCheckIn")}
- />
- </div>
- <div>
- <Checkbox
- label="Late checkout"
- {...methods.register("guestsRequests.lateCheckout")}
- />
- </div>
- </div>
- <label className="block mt-6">
- <Text tag="span" className="block mb-3">
- Anything else you would like us to inform your hotel?
- </Text>
- <textarea
- {...methods.register("guestsRequests.remarks")}
- placeholder="Add your note..."
- className="w-full h-32 rounded p-4 outline-none border border-neutral-20 bg-input focus:border-primary"
- />
- </label>
- </section>
- </div>
- <aside className="col-span-1">
- <div className="border border-neutral-20 shadow-xl rounded-sm mt-7 md:sticky top-0">
- <div className="border-b-1 border-neutral-20">
- <div className="bg-white md:px-4">
- <Title tag="h4" className="py-3 text-16-semibold">
- Your Exclusive Deals
- </Title>
- </div>
- </div>
- <Voucher
- couponDiscount={couponDiscount}
- onDiscountChange={setCouponDiscountPercentage}
- />
- </div>
- <div className="border border-neutral-20 shadow-xl rounded-sm mt-7 md:sticky top-0">
- <div className="bg-white p-2 md:p-4">
- <Title tag="h4" className="mb-4 text-24-semibold">
- Price Breakdown
- </Title>
- <ul className="space-y-4">
- <ListItem
- container="li"
- className="items-start"
- textContainerClasses="gap-0"
- title={`1 room X ${nrOfNights.formatted}`}
- subtitle={
- <>
- <Text tag="span" className="text-neutral-50 text-xs">
- {formatMoney({
- currency: prebook.currency,
- amount: prebook.price / nrOfNights.amount,
- })}{" "}
- per night
- </Text>
- {includedTaxesAndFees?.length && (
- <Text tag="p" className="text-neutral-50 text-xs">
- Inclusive of{" "}
- {includedTaxesAndFees
- .map((tax) => tax.description)
- .join(", ")}
- </Text>
- )}
- </>
- }
- endItem={formatMoney({
- currency: prebook.currency,
- amount: prebook.price,
- })}
- />
- </ul>
- {isKlubPointSelected && loyalty?.equivalentAmount && (
- <div className="border-t border-netrual-70 text-green-dark pt-4 mt-4 font-semibold">
- <ListItem
- title={
- <div className="text-16-semibold">
- Klub Points Discount
- </div>
- }
- endItem={
- <>
- -{" "}
- {formatMoney({
- currency: prebook.currency,
- amount: loyalty.equivalentAmount,
- })}
- </>
- }
- container="div"
- />
- </div>
- )}
- {!!discountPercentage && (
- <div className="border-y border-dotted border-netrual-70 py-4 my-4">
- <ListItem
- endTextClasses="text-red-500"
- title="Promo Code Applied"
- endItem={
- <>
- -{" "}
- {formatMoney({
- currency: prebook.currency,
- amount: couponDiscount,
- })}
- </>
- }
- />
- </div>
- )}
- <div className="border-y border-dotted border-netrual-70 py-4 my-4">
- <ListItem
- title={<div className="text-16-semibold">Total amount</div>}
- endItem={formatMoney({
- currency: prebook.currency,
- amount: totalAmount,
- })}
- container="div"
- />
- </div>
- <Button
- type="submit"
- size="medium"
- className="mt-4"
- isLoading={
- mutation.isPending || isTransitioning || paymentSessionUrl
- }
- >
- Go to Payment
- </Button>
- </div>
- {!!exludedTaxesAndFees?.length && (
- <div className="p-4 mt-4 bg-neutral-20 rounded-sm">
- <div className="text-16-regular mb-1">Additional Charges</div>
- <div className="text-12-regular text-neutral-50 mb-2">
- This includes taxes to be paid at the hotel desk during
- check in.
- </div>
- <ul className="space-y-1">
- {exludedTaxesAndFees.map((tax, index: number) => (
- <ListItem
- key={`tax-${index}`}
- title={tax.description}
- endItem={formatMoney({
- currency: tax.currency,
- amount: tax.amount,
- })}
- container="li"
- />
- ))}
- </ul>
- </div>
- )}
- </div>
- </aside>
- </div>
- </form>
- </FormProvider>
- );
- };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement