diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 9737ea82a..236acc354 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1,17 +1,13 @@ import { notFound } from "next/navigation" import { Suspense } from "react" -import { getLocations } from "@/lib/trpc/memoizedRequests" - import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" -import { - generateChildrenString, - getHotelReservationQueryParams, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { MapContainer } from "@/components/MapContainer" import { setLang } from "@/i18n/serverContext" +import { getHotelSearchDetails } from "../../utils" + import styles from "./page.module.css" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" @@ -22,26 +18,13 @@ export default async function SelectHotelMapPage({ searchParams, }: PageArgs) { setLang(params.lang) - const locations = await getLocations() + const searchDetails = await getHotelSearchDetails({ searchParams }) + if (!searchDetails) return notFound() + const { city, adultsInRoom, childrenInRoom, childrenInRoomArray } = + searchDetails - if (!locations || "error" in locations) { - return null - } - const city = locations.data.find( - (location) => - location.name.toLowerCase() === searchParams.city.toLowerCase() - ) if (!city) return notFound() - const selectHotelParams = new URLSearchParams(searchParams) - const selectHotelParamsObject = - getHotelReservationQueryParams(selectHotelParams) - const adultsInRoom = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const childrenInRoom = selectHotelParamsObject.room[0].child - ? generateChildrenString(selectHotelParamsObject.room[0].child) - : undefined // TODO: Handle multiple rooms - const child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms - return (
@@ -54,7 +37,7 @@ export default async function SelectHotelMapPage({ searchParams={searchParams} adultsInRoom={adultsInRoom} childrenInRoom={childrenInRoom} - child={child} + child={childrenInRoomArray} /> diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 9888a05f6..ddde1cf40 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,16 +1,13 @@ import { notFound } from "next/navigation" import { Suspense } from "react" -import { getLocations } from "@/lib/trpc/memoizedRequests" - import SelectHotel from "@/components/HotelReservation/SelectHotel" import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton" -import { - generateChildrenString, - getHotelReservationQueryParams, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { getHotelSearchDetails } from "../utils" + import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" @@ -19,46 +16,29 @@ export default async function SelectHotelPage({ searchParams, }: PageArgs) { setLang(params.lang) - const locations = await getLocations() - - if (!locations || "error" in locations) { - return null - } - const city = locations.data.find( - (location) => - location.name.toLowerCase() === searchParams.city.toLowerCase() - ) + const searchDetails = await getHotelSearchDetails({ searchParams }) + if (!searchDetails) return notFound() + const { + city, + urlSearchParams, + adultsInRoom, + childrenInRoom, + childrenInRoomArray, + } = searchDetails if (!city) return notFound() - const selectHotelParams = new URLSearchParams(searchParams) - const selectHotelParamsObject = - getHotelReservationQueryParams(selectHotelParams) - - if ( - !selectHotelParamsObject.room || - selectHotelParamsObject.room.length === 0 - ) { - return notFound() - } - - const adultsParams = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const childrenParams = selectHotelParamsObject.room[0].child - ? generateChildrenString(selectHotelParamsObject.room[0].child) - : undefined // TODO: Handle multiple rooms - const child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms - const reservationParams = { - selectHotelParams, + selectHotelParams: urlSearchParams, searchParams, - adultsParams, - childrenParams, - child, + adultsInRoom, + childrenInRoom, + childrenInRoomArray, } return ( } > ) { setLang(params.lang) + const searchDetails = await getHotelSearchDetails({ searchParams }) + if (!searchDetails) return notFound() + const { hotel, adultsInRoom, childrenInRoomArray } = searchDetails - const locations = await getLocations() - if (!locations || "error" in locations) { - return null - } - const hotel = locations.data.find( - (location) => - "operaId" in location && location.operaId == searchParams.hotel - ) - if (!hotel) { - return notFound() - } - const selectRoomParams = new URLSearchParams(searchParams) - const selectRoomParamsObject = - getHotelReservationQueryParams(selectRoomParams) + if (!hotel) return notFound() - if (!selectRoomParamsObject.room) { - return notFound() - } + const hotelData = await getHotelData({ + hotelId: hotel.id, + language: params.lang, + }) const { fromDate, toDate } = getValidDates( searchParams.fromDate, searchParams.toDate ) - const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room[0].child // TODO: Handle multiple rooms - - const [hotelData, hotelDataError] = await safeTry( - getHotelData({ hotelId: searchParams.hotel, language: params.lang }) - ) - - if (!hotelData && !hotelDataError) { - return notFound() - } const arrivalDate = new Date(searchParams.fromDate) const departureDate = new Date(searchParams.toDate) - const hotelAttributes = hotelData?.data.attributes - - const roomCategories = hotelData?.included const pageTrackingData: TrackingSDKPageData = { pageId: "select-rate", @@ -80,26 +57,28 @@ export default async function SelectRatePage({ } const hotelsTrackingData: TrackingSDKHotelInfo = { - searchTerm: searchParams.city ?? hotelAttributes?.name, + searchTerm: searchParams.city ?? hotel?.name, arrivalDate: format(arrivalDate, "yyyy-MM-dd"), departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: adults, - noOfChildren: children?.length, - ageOfChildren: children?.map((c) => c.age).join(","), - childBedPreference: children?.map((c) => ChildBedMapEnum[c.bed]).join("|"), + noOfAdults: adultsInRoom, + noOfChildren: childrenInRoomArray?.length, + ageOfChildren: childrenInRoomArray?.map((c) => c.age).join(","), + childBedPreference: childrenInRoomArray + ?.map((c) => ChildBedMapEnum[c.bed]) + .join("|"), noOfRooms: 1, // // TODO: Handle multiple rooms duration: differenceInCalendarDays(departureDate, arrivalDate), leadTime: differenceInCalendarDays(arrivalDate, new Date()), searchType: "hotel", bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: hotelAttributes?.address.country, - hotelID: hotelAttributes?.operaId, - region: hotelAttributes?.address.city, - availableResults: roomCategories?.length, + country: hotelData?.data?.attributes.address.country, + hotelID: hotel?.id, + region: hotelData?.data?.attributes.address.city, + //availableResults: roomCategories?.length, //lowestRoomPrice: } - const hotelId = +searchParams.hotel + const hotelId = +hotel.id return ( <> @@ -108,8 +87,8 @@ export default async function SelectRatePage({ lang={params.lang} fromDate={fromDate.toDate()} toDate={toDate.toDate()} - adultCount={adults} - childArray={children} + adultCount={adultsInRoom} + childArray={childrenInRoomArray} /> }> @@ -118,8 +97,8 @@ export default async function SelectRatePage({ lang={params.lang} fromDate={fromDate.toDate()} toDate={toDate.toDate()} - adultCount={adults} - childArray={children} + adultCount={adultsInRoom} + childArray={childrenInRoomArray} /> diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts new file mode 100644 index 000000000..2a0ae8d42 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts @@ -0,0 +1,77 @@ +import { notFound } from "next/navigation" + +import { getLocations } from "@/lib/trpc/memoizedRequests" + +import { + generateChildrenString, + getHotelReservationQueryParams, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" + +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { + Child, + SelectRateSearchParams, +} from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Location } from "@/types/trpc/routers/hotel/locations" + +interface HotelSearchDetails { + city: Location | null + hotel: Location | null + urlSearchParams?: URLSearchParams + adultsInRoom: number + childrenInRoom?: string + childrenInRoomArray?: Child[] +} + +export async function getHotelSearchDetails({ + searchParams, +}: { + searchParams: + | (SelectHotelSearchParams & { + [key: string]: string + }) + | (SelectRateSearchParams & { + [key: string]: string + }) +}): Promise { + const locations = await getLocations() + + if (!locations || "error" in locations) return null + + const city = locations.data.find( + (location) => + location.name.toLowerCase() === searchParams.city?.toLowerCase() + ) + const hotel = locations.data.find( + (location) => + "operaId" in location && location.operaId == searchParams.hotel + ) + + if (!city && !hotel) return notFound() + + const urlSearchParams = new URLSearchParams(searchParams) + const searchParamsObject = getHotelReservationQueryParams(urlSearchParams) + + let adultsInRoom = 1 + let childrenInRoom: string | undefined = undefined + let childrenInRoomArray: Child[] | undefined = undefined + + if (searchParamsObject.room && searchParamsObject.room.length > 0) { + adultsInRoom = searchParamsObject.room[0].adults // TODO: Handle multiple rooms + childrenInRoom = searchParamsObject.room[0].child + ? generateChildrenString(searchParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms + childrenInRoomArray = searchParamsObject.room[0].child + ? searchParamsObject.room[0].child + : undefined // TODO: Handle multiple rooms + } + + return { + city: city ?? null, + hotel: hotel ?? null, + urlSearchParams, + adultsInRoom, + childrenInRoom, + childrenInRoomArray, + } +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx index 761682e83..693b8a409 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx @@ -17,6 +17,7 @@ export default function ClientCurrentRewards({ rewards, pageSize, showRedeem, + membershipNumber, }: CurrentRewardsClientProps) { const containerRef = useRef(null) const [currentPage, setCurrentPage] = useState(1) @@ -55,7 +56,7 @@ export default function ClientCurrentRewards({
{showRedeem && "redeem_description" in reward && (
- +
)} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx index 5686ad8a0..1c18fea56 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/Redeem.tsx @@ -18,6 +18,7 @@ import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" +import { isRestaurantOnSiteTierReward } from "@/utils/rewards" import { RewardIcon } from "../RewardIcon" @@ -28,11 +29,12 @@ import type { RedeemProps, RedeemStep, } from "@/types/components/myPages/myPage/accountPage" +import type { Reward } from "@/server/routers/contentstack/reward/output" const MotionOverlay = motion(ModalOverlay) const MotionModal = motion(Modal) -export default function Redeem({ reward }: RedeemProps) { +export default function Redeem({ reward, membershipNumber }: RedeemProps) { const [animation, setAnimation] = useState("unmounted") const intl = useIntl() const update = trpc.contentstack.rewards.redeem.useMutation() @@ -100,17 +102,7 @@ export default function Redeem({ reward }: RedeemProps) {
{redeemStep === "redeemed" && ( -
-
- - - {intl.formatMessage({ - id: "Redeemed & valid through:", - })} - -
- -
+ )} @@ -127,6 +119,13 @@ export default function Redeem({ reward }: RedeemProps) { {reward.redeem_description} </Body> )} + {redeemStep === "redeemed" && + isRestaurantOnSiteTierReward(reward) && + membershipNumber && ( + <MembershipNumberBadge + membershipNumber={membershipNumber} + /> + )} </div> {redeemStep === "initial" && ( <footer className={styles.modalFooter}> @@ -189,3 +188,75 @@ const variants = { }, }, } + +function ConfirmationBadge({ reward }: { reward: Reward }) { + return ( + <div className={styles.badge}> + {isRestaurantOnSiteTierReward(reward) ? ( + <ActiveRedeemedBadge /> + ) : ( + <TimedRedeemedBadge /> + )} + </div> + ) +} + +function ActiveRedeemedBadge() { + const intl = useIntl() + + return ( + <div className={styles.redeemed}> + <motion.div + animate={{ + opacity: [1, 0.4, 1], + }} + transition={{ + duration: 2, + repeat: Infinity, + ease: "easeInOut", + }} + > + <CheckCircleIcon color="uiSemanticSuccess" /> + </motion.div> + <Caption>{intl.formatMessage({ id: "Active" })}</Caption> + </div> + ) +} + +function TimedRedeemedBadge() { + const intl = useIntl() + + return ( + <> + <div className={styles.redeemed}> + <CheckCircleIcon color="uiSemanticSuccess" /> + <Caption> + {intl.formatMessage({ + id: "Redeemed & valid through:", + })} + </Caption> + </div> + <Countdown /> + </> + ) +} + +function MembershipNumberBadge({ + membershipNumber, +}: { + membershipNumber: string +}) { + const intl = useIntl() + + return ( + <div className={styles.membershipNumberBadge}> + <Caption + textTransform="uppercase" + textAlign="center" + color="uiTextHighContrast" + > + {intl.formatMessage({ id: "Membership ID:" })} {membershipNumber} + </Caption> + </div> + ) +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css b/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css index da85887a0..605e8835b 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/current.module.css @@ -130,3 +130,16 @@ display: flex; align-items: center; } + +.active { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); + color: var(--UI-Semantic-Success); +} + +.membershipNumberBadge { + border-radius: var(--Small); + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + background: var(--Base-Surface-Secondary-light-Normal); +} diff --git a/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx b/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx index 8dd47b612..62eff7a54 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx @@ -1,5 +1,8 @@ import { env } from "@/env/server" -import { serverClient } from "@/lib/trpc/server" +import { + getCurrentRewards, + getMembershipLevel, +} from "@/lib/trpc/memoizedRequests" import SectionContainer from "@/components/Section/Container" import SectionHeader from "@/components/Section/Header" @@ -14,7 +17,10 @@ export default async function CurrentRewardsBlock({ subtitle, link, }: AccountPageComponentProps) { - const rewardsResponse = await serverClient().contentstack.rewards.current() + const [rewardsResponse, membershipLevel] = await Promise.all([ + getCurrentRewards(), + getMembershipLevel(), + ]) if (!rewardsResponse?.rewards.length) { return null @@ -27,6 +33,7 @@ export default async function CurrentRewardsBlock({ rewards={rewardsResponse.rewards} pageSize={6} showRedeem={env.USE_NEW_REWARDS_ENDPOINT && env.USE_NEW_REWARD_MODEL} + membershipNumber={membershipLevel?.membershipNumber} /> <SectionLink link={link} variant="mobile" /> </SectionContainer> diff --git a/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts b/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts index fcb388b64..11aedfc09 100644 --- a/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts +++ b/components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts @@ -1,53 +1,55 @@ +import { REWARD_IDS } from "@/constants/rewards" + import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import { isValidRewardId } from "@/utils/rewards" import type { FC } from "react" import { IconName, type IconProps } from "@/types/components/icon" -import { RewardId } from "@/types/enums/rewards" +import type { RewardId } from "@/types/components/myPages/rewards" function getIconForRewardId(rewardId: RewardId): IconName { switch (rewardId) { // Food & beverage - case RewardId.TenPercentFood: - case RewardId.FifteenPercentFood: + case REWARD_IDS.TenPercentFood: + case REWARD_IDS.FifteenPercentFood: return IconName.CroissantCoffeeEgg - case RewardId.TwoForOneBreakfast: + case REWARD_IDS.TwoForOneBreakfast: return IconName.CutleryTwo - case RewardId.FreeBreakfast: + case REWARD_IDS.FreeBreakfast: return IconName.CutleryOne - case RewardId.FreeKidsDrink: + case REWARD_IDS.FreeKidsDrink: return IconName.KidsMocktail // Monetary vouchers - case RewardId.Bonus50SEK: - case RewardId.Bonus75SEK: - case RewardId.Bonus100SEK: - case RewardId.Bonus150SEK: - case RewardId.Bonus200SEK: + case REWARD_IDS.Bonus50SEK: + case REWARD_IDS.Bonus75SEK: + case REWARD_IDS.Bonus100SEK: + case REWARD_IDS.Bonus150SEK: + case REWARD_IDS.Bonus200SEK: return IconName.Voucher // Hotel perks - case RewardId.EarlyCheckin: + case REWARD_IDS.EarlyCheckin: return IconName.HandKey - case RewardId.LateCheckout: + case REWARD_IDS.LateCheckout: return IconName.HotelNight - case RewardId.FreeUpgrade: + case REWARD_IDS.FreeUpgrade: return IconName.MagicWand - case RewardId.RoomGuarantee48H: + case REWARD_IDS.RoomGuarantee48H: return IconName.Bed // Earnings - case RewardId.EarnRate25Percent: - case RewardId.EarnRate50Percent: + case REWARD_IDS.EarnRate25Percent: + case REWARD_IDS.EarnRate50Percent: return IconName.MoneyHand - case RewardId.StayBoostForKids: + case REWARD_IDS.StayBoostForKids: return IconName.Kids - case RewardId.MemberRate: + case REWARD_IDS.MemberRate: return IconName.Coin // Special - case RewardId.YearlyExclusiveGift: + case REWARD_IDS.YearlyExclusiveGift: return IconName.GiftOpen default: { diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index c93d26c9b..b66c26567 100644 --- a/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -218,7 +218,9 @@ export default function PaymentClient({ bedType: bedTypeMap[parseInt(child.bed.toString())], })), rateCode: - user || join || membershipNo ? room.counterRateCode : room.rateCode, + (user || join || membershipNo) && room.counterRateCode + ? room.counterRateCode + : room.rateCode, roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. guest: { firstName, diff --git a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 5a84dde6e..90dc0775d 100644 --- a/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -67,7 +67,7 @@ export default function SummaryUI({ } : null - const showMemberPrice = !!(isMember || join || membershipNo) + const showMemberPrice = !!(isMember || join || membershipNo) && memberPrice const diff = dt(booking.toDate).diff(booking.fromDate, "days") diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index c7687362b..353bf2588 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,6 +1,6 @@ "use client" import { useSearchParams } from "next/navigation" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useMemo } from "react" import { useIntl } from "react-intl" import { useHotelFilterStore } from "@/stores/hotel-filters" @@ -8,18 +8,18 @@ import { useHotelsMapStore } from "@/stores/hotels-map" import Alert from "@/components/TempDesignSystem/Alert" import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" +import { useScrollToTop } from "@/hooks/useScrollToTop" import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" +import { getSortedHotels } from "./utils" import styles from "./hotelCardListing.module.css" import { type HotelCardListingProps, HotelCardListingTypeEnum, - type HotelData, } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" -import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" import { AlertTypeEnum } from "@/types/enums/alert" export default function HotelCardListing({ @@ -29,82 +29,36 @@ export default function HotelCardListing({ const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) - const [showBackToTop, setShowBackToTop] = useState<boolean>(false) const intl = useIntl() const { activeHotelCard } = useHotelsMapStore() + const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 }) const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, [searchParams] ) - const sortedHotels = useMemo(() => { - switch (sortBy) { - case SortOrder.Name: - return [...hotelData].sort((a, b) => - a.hotelData.name.localeCompare(b.hotelData.name) - ) - case SortOrder.TripAdvisorRating: - return [...hotelData].sort( - (a, b) => - (b.hotelData.ratings?.tripAdvisor.rating ?? 0) - - (a.hotelData.ratings?.tripAdvisor.rating ?? 0) - ) - case SortOrder.Price: - const getPricePerNight = (hotel: HotelData): number => { - return ( - hotel.price?.member?.localPrice?.pricePerNight ?? - hotel.price?.public?.localPrice?.pricePerNight ?? - Infinity - ) - } - return [...hotelData].sort( - (a, b) => getPricePerNight(a) - getPricePerNight(b) - ) - case SortOrder.Distance: - default: - return [...hotelData].sort( - (a, b) => - a.hotelData.location.distanceToCentre - - b.hotelData.location.distanceToCentre - ) - } - }, [hotelData, sortBy]) + const sortedHotels = useMemo( + () => getSortedHotels({ hotels: hotelData, sortBy }), + [hotelData, sortBy] + ) const hotels = useMemo(() => { - if (activeFilters.length === 0) { - return sortedHotels - } + if (activeFilters.length === 0) return sortedHotels - const filteredHotels = sortedHotels.filter((hotel) => + return sortedHotels.filter((hotel) => activeFilters.every((appliedFilterId) => hotel.hotelData.detailedFacilities.some( (facility) => facility.id.toString() === appliedFilterId ) ) ) - - return filteredHotels }, [activeFilters, sortedHotels]) useEffect(() => { - const handleScroll = () => { - const hasScrolledPast = window.scrollY > 490 - setShowBackToTop(hasScrolledPast) - } - - window.addEventListener("scroll", handleScroll, { passive: true }) - return () => window.removeEventListener("scroll", handleScroll) - }, []) - - useEffect(() => { - setResultCount(hotels ? hotels.length : 0) + setResultCount(hotels?.length ?? 0) }, [hotels, setResultCount]) - function scrollToTop() { - window.scrollTo({ top: 0, behavior: "smooth" }) - } - return ( <section className={styles.hotelCards}> {hotels?.length ? ( diff --git a/components/HotelReservation/HotelCardListing/utils.ts b/components/HotelReservation/HotelCardListing/utils.ts new file mode 100644 index 000000000..1e57a6721 --- /dev/null +++ b/components/HotelReservation/HotelCardListing/utils.ts @@ -0,0 +1,35 @@ +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" + +export function getSortedHotels({ + hotels, + sortBy, +}: { + hotels: HotelData[] + sortBy: string +}) { + const getPricePerNight = (hotel: HotelData): number => + hotel.price?.member?.localPrice?.pricePerNight ?? + hotel.price?.public?.localPrice?.pricePerNight ?? + Infinity + + const sortingStrategies: Record< + string, + (a: HotelData, b: HotelData) => number + > = { + [SortOrder.Name]: (a: HotelData, b: HotelData) => + a.hotelData.name.localeCompare(b.hotelData.name), + [SortOrder.TripAdvisorRating]: (a: HotelData, b: HotelData) => + (b.hotelData.ratings?.tripAdvisor.rating ?? 0) - + (a.hotelData.ratings?.tripAdvisor.rating ?? 0), + [SortOrder.Price]: (a: HotelData, b: HotelData) => + getPricePerNight(a) - getPricePerNight(b), + [SortOrder.Distance]: (a: HotelData, b: HotelData) => + a.hotelData.location.distanceToCentre - + b.hotelData.location.distanceToCentre, + } + + return [...hotels].sort( + sortingStrategies[sortBy] ?? sortingStrategies[SortOrder.Distance] + ) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx index ed496c4cf..a9b72d220 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -15,6 +15,7 @@ import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import useLang from "@/hooks/useLang" +import { useScrollToTop } from "@/hooks/useScrollToTop" import { debounce } from "@/utils/debounce" import FilterAndSortModal from "../../FilterAndSortModal" @@ -41,13 +42,17 @@ export default function SelectHotelContent({ const isAboveMobile = useMediaQuery("(min-width: 768px)") const [visibleHotels, setVisibleHotels] = useState<HotelData[]>([]) - const [showBackToTop, setShowBackToTop] = useState<boolean>(false) - const [showSkeleton, setShowSkeleton] = useState<boolean>(false) + const [showSkeleton, setShowSkeleton] = useState<boolean>(true) const listingContainerRef = useRef<HTMLDivElement | null>(null) const activeFilters = useHotelFilterStore((state) => state.activeFilters) const { activeHotelCard, activeHotelPin } = useHotelsMapStore() + const { showBackToTop, scrollToTop } = useScrollToTop({ + threshold: 490, + elementRef: listingContainerRef, + }) + const coordinates = useMemo( () => isAboveMobile @@ -66,28 +71,6 @@ export default function SelectHotelContent({ } }, [activeHotelCard, activeHotelPin]) - useEffect(() => { - const hotelListingElement = document.querySelector( - `.${styles.listingContainer}` - ) - if (!hotelListingElement) return - - const handleScroll = () => { - const hasScrolledPast = hotelListingElement.scrollTop > 490 - setShowBackToTop(hasScrolledPast) - } - - hotelListingElement.addEventListener("scroll", handleScroll) - return () => hotelListingElement.removeEventListener("scroll", handleScroll) - }, []) - - function scrollToTop() { - const hotelListingElement = document.querySelector( - `.${styles.listingContainer}` - ) - hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" }) - } - const filteredHotelPins = useMemo( () => hotelPins.filter((hotel) => @@ -102,7 +85,7 @@ export default function SelectHotelContent({ const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map) setVisibleHotels(visibleHotels) setTimeout(() => { - setShowSkeleton(true) + setShowSkeleton(false) }, SKELETON_LOAD_DELAY) }, [hotels, filteredHotelPins, map]) @@ -116,7 +99,7 @@ export default function SelectHotelContent({ () => debounce(() => { if (!map) return - setShowSkeleton(false) + setShowSkeleton(true) getHotelCards() }, 100), [map, getHotelCards] @@ -155,11 +138,12 @@ export default function SelectHotelContent({ </Button> <FilterAndSortModal filters={filterList} /> </div> + {showSkeleton ? ( - <> + <div className={styles.skeletonContainer}> <RoomCardSkeleton /> <RoomCardSkeleton /> - </> + </div> ) : ( <HotelListing hotels={visibleHotels} /> )} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css index 811f94d68..ae6b81191 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css @@ -48,4 +48,10 @@ padding: 0 0 var(--Spacing-x1); position: static; } + + .skeletonContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + } } diff --git a/components/HotelReservation/SelectHotel/index.tsx b/components/HotelReservation/SelectHotel/index.tsx index 7ce5e7cbb..861d2a9ef 100644 --- a/components/HotelReservation/SelectHotel/index.tsx +++ b/components/HotelReservation/SelectHotel/index.tsx @@ -47,9 +47,9 @@ export default async function SelectHotel({ const { selectHotelParams, searchParams, - adultsParams, - childrenParams, - child, + adultsInRoom, + childrenInRoom, + childrenInRoomArray, } = reservationParams const intl = await getIntl() @@ -59,8 +59,8 @@ export default async function SelectHotel({ cityId: city.id, roomStayStartDate: searchParams.fromDate, roomStayEndDate: searchParams.toDate, - adults: adultsParams, - children: childrenParams?.toString(), + adults: adultsInRoom, + children: childrenInRoom, }) ) @@ -115,10 +115,12 @@ export default async function SelectHotel({ searchTerm: searchParams.city, arrivalDate: format(arrivalDate, "yyyy-MM-dd"), departureDate: format(departureDate, "yyyy-MM-dd"), - noOfAdults: adultsParams, - noOfChildren: child?.length, - ageOfChildren: child?.map((c) => c.age).join(","), - childBedPreference: child?.map((c) => ChildBedMapEnum[c.bed]).join("|"), + noOfAdults: adultsInRoom, + noOfChildren: childrenInRoomArray?.length, + ageOfChildren: childrenInRoomArray?.map((c) => c.age).join(","), + childBedPreference: childrenInRoomArray + ?.map((c) => ChildBedMapEnum[c.bed]) + .join("|"), noOfRooms: 1, // // TODO: Handle multiple rooms duration: differenceInCalendarDays(departureDate, arrivalDate), leadTime: differenceInCalendarDays(arrivalDate, new Date()), diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index a201a44b1..eff2e3717 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -28,7 +28,10 @@ export default function PriceList({ const petRoomLocalPrice = petRoomPackage?.localPrice const petRoomRequestedPrice = petRoomPackage?.requestedPrice - const showRequestedPrice = publicRequestedPrice && memberRequestedPrice + const showRequestedPrice = + publicRequestedPrice && + memberRequestedPrice && + publicRequestedPrice.currency !== publicLocalPrice.currency const searchParams = useSearchParams() const fromDate = searchParams.get("fromDate") const toDate = searchParams.get("toDate") @@ -114,27 +117,22 @@ export default function PriceList({ )} </dd> </div> - - <div className={styles.priceRow}> - <dt> - <Caption - color={showRequestedPrice ? "uiTextMediumContrast" : "disabled"} - > - {intl.formatMessage({ id: "Approx." })} - </Caption> - </dt> - <dd> - {showRequestedPrice ? ( + {showRequestedPrice && ( + <div className={styles.priceRow}> + <dt> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage({ id: "Approx." })} + </Caption> + </dt> + <dd> <Caption color="uiTextMediumContrast"> {totalPublicRequestedPricePerNight}/ {totalMemberRequestedPricePerNight}{" "} {publicRequestedPrice.currency} </Caption> - ) : ( - <Caption color="disabled">- / - EUR</Caption> - )} - </dd> - </div> + </dd> + </div> + )} </dl> ) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 1a227d568..5e13bd1e9 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -7,6 +7,7 @@ import Label from "@/components/TempDesignSystem/Form/Label" import Popover from "@/components/TempDesignSystem/Popover" import Caption from "@/components/TempDesignSystem/Text/Caption" +import { rateCardEqualHeightSelector } from "../utils" import PriceTable from "./PriceList" import styles from "./flexibilityOption.module.css" @@ -26,11 +27,13 @@ export default function FlexibilityOption({ if (!product) { return ( - <div className={styles.noPricesCard}> + <div className={`${styles.noPricesCard} ${rateCardEqualHeightSelector}`}> <div className={styles.header}> <InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" /> - <Caption>{name}</Caption> - <Caption color="uiTextPlaceholder">({paymentTerm})</Caption> + <div className={styles.priceType}> + <Caption>{name}</Caption> + <Caption color="uiTextPlaceholder">({paymentTerm})</Caption> + </div> </div> <Label size="regular" className={styles.noPricesLabel}> <Caption color="uiTextHighContrast" type="bold"> @@ -70,7 +73,7 @@ export default function FlexibilityOption({ value={publicPrice?.rateCode} onClick={onClick} /> - <div className={styles.card}> + <div className={`${styles.card} ${rateCardEqualHeightSelector}`}> <div className={styles.header}> <Popover placement="bottom left" diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 9f63b4923..3c6b75707 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -1,10 +1,15 @@ "use client" import { useRouter, useSearchParams } from "next/navigation" -import { useMemo } from "react" +import { useCallback, useEffect, useMemo, useRef } from "react" + +import { debounce } from "@/utils/debounce" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" -import { getHotelReservationQueryParams } from "./utils" +import { + getHotelReservationQueryParams, + rateCardEqualHeightSelector, +} from "./utils" import styles from "./roomSelection.module.css" @@ -23,9 +28,65 @@ export default function RoomSelection({ const router = useRouter() const searchParams = useSearchParams() const isUserLoggedIn = !!user - + const roomRefs = useRef<HTMLLIElement[]>([]) const { roomConfigurations, rateDefinitions } = roomsAvailability + const equalizePriceOptionHeights = useCallback(() => { + if (!roomRefs.current.length) return + + roomRefs.current.forEach((room) => { + const options = room.querySelectorAll<HTMLDivElement>( + `.${rateCardEqualHeightSelector}` + ) + options.forEach((option) => { + option.style.height = "auto" + }) + }) + + const numOptions = + roomRefs.current[0]?.querySelectorAll<HTMLDivElement>( + `.${rateCardEqualHeightSelector}` + ).length || 0 + + for (let i = 0; i < numOptions; i++) { + let maxHeight = 0 + + roomRefs.current.forEach((room) => { + const option = room.querySelectorAll<HTMLDivElement>( + `.${rateCardEqualHeightSelector}` + )[i] + if (option) { + maxHeight = Math.max(maxHeight, option.offsetHeight) + } + }) + + roomRefs.current.forEach((room) => { + const option = room.querySelectorAll<HTMLDivElement>( + `.${rateCardEqualHeightSelector}` + )[i] + if (option) { + option.style.height = `${maxHeight}px` + } + }) + } + }, []) + + useEffect(() => { + const debouncedResizeHandler = debounce(function () { + equalizePriceOptionHeights() + }) + + const observer = new ResizeObserver(debouncedResizeHandler) + + observer.observe(document.documentElement) + + return () => { + if (observer) { + observer.unobserve(document.documentElement) + } + } + }, [roomRefs, equalizePriceOptionHeights]) + const queryParams = useMemo(() => { const params = new URLSearchParams(searchParams) const searchParamsObject = getHotelReservationQueryParams(searchParams) @@ -64,8 +125,13 @@ export default function RoomSelection({ onSubmit={handleSubmit} > <ul className={styles.roomList}> - {roomConfigurations.map((roomConfiguration) => ( - <li key={roomConfiguration.roomTypeCode}> + {roomConfigurations.map((roomConfiguration, index) => ( + <li + key={roomConfiguration.roomTypeCode} + ref={(el) => { + if (el) roomRefs.current[index] = el + }} + > <RoomCard hotelId={roomsAvailability.hotelId.toString()} hotelType={hotelType} diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index a37da6eb0..92ef0be01 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -101,3 +101,5 @@ export function createQueryParamsForEnterDetails( return searchParams } + +export const rateCardEqualHeightSelector = "rateCardEqualHeight" diff --git a/constants/rewards.ts b/constants/rewards.ts new file mode 100644 index 000000000..56a743ca4 --- /dev/null +++ b/constants/rewards.ts @@ -0,0 +1,39 @@ +export const REWARD_IDS = { + // Food & Beverage + TenPercentFood: "tier_10_percent_food_tier", + TwoForOneBreakfast: "tier_2_for_one_breakfast", + FifteenPercentFood: "tier_15_percent_food", + FreeKidsDrink: "tier_free_kids_drink", + FreeBreakfast: "tier_free_breakfast", + + // Monetary Vouchers + Bonus50SEK: "tier_50_SEK_bonus_voucher", + Bonus75SEK: "tier_75_SEK_bonus_voucher", + Bonus100SEK: "tier_100_SEK_bonus_voucher", + Bonus150SEK: "tier_150_SEK_bonus_voucher", + Bonus200SEK: "tier_200_SEK_bonus_voucher", + + // Hotel Perks + EarlyCheckin: "tier_early_checkin_tier", + LateCheckout: "tier_late_checkout", + FreeUpgrade: "tier_free_upgrade", + RoomGuarantee48H: "tier_48_h_room_guarantee", + // GymAccess: "tier_gym_access", + + // Earning & Points + EarnRate25Percent: "tier_25_percent_earn_rate", + EarnRate50Percent: "tier_50_percent_earn_rate", + StayBoostForKids: "tier_stay_boost_for_kids", + MemberRate: "tier_member_rate", + + // Special + YearlyExclusiveGift: "tier_yearly_exclusive_gift", +} as const + +export const RESTAURANT_REWARD_IDS = [ + REWARD_IDS.TenPercentFood, + REWARD_IDS.TwoForOneBreakfast, + REWARD_IDS.FifteenPercentFood, + REWARD_IDS.FreeKidsDrink, + REWARD_IDS.FreeBreakfast, +] as const diff --git a/hooks/booking/usePaymentFailedToast.ts b/hooks/booking/usePaymentFailedToast.ts index 961ab0c2b..806c3d5a2 100644 --- a/hooks/booking/usePaymentFailedToast.ts +++ b/hooks/booking/usePaymentFailedToast.ts @@ -5,10 +5,14 @@ import { useCallback, useEffect } from "react" import { useIntl } from "react-intl" import { PaymentErrorCodeEnum } from "@/constants/booking" +import { useEnterDetailsStore } from "@/stores/enter-details" import { toast } from "@/components/TempDesignSystem/Toasts" export function usePaymentFailedToast() { + const updateSearchParams = useEnterDetailsStore( + (state) => state.actions.updateSeachParamString + ) const intl = useIntl() const searchParams = useSearchParams() const pathname = usePathname() @@ -43,6 +47,15 @@ export function usePaymentFailedToast() { const queryParams = new URLSearchParams(searchParams.toString()) queryParams.delete("errorCode") + + updateSearchParams(queryParams.toString()) router.push(`${pathname}?${queryParams.toString()}`) - }, [searchParams, pathname, errorCode, errorMessage, router]) + }, [ + searchParams, + pathname, + errorCode, + errorMessage, + router, + updateSearchParams, + ]) } diff --git a/hooks/useScrollToTop.ts b/hooks/useScrollToTop.ts new file mode 100644 index 000000000..39ff5da0c --- /dev/null +++ b/hooks/useScrollToTop.ts @@ -0,0 +1,32 @@ +import { type RefObject, useEffect, useState } from "react" + +interface UseScrollToTopProps { + threshold: number + elementRef?: RefObject<HTMLElement> +} + +export function useScrollToTop({ threshold, elementRef }: UseScrollToTopProps) { + const [showBackToTop, setShowBackToTop] = useState(false) + + useEffect(() => { + const element = elementRef?.current ?? window + + function handleScroll() { + const scrollTop = elementRef?.current + ? elementRef.current.scrollTop + : window.scrollY + setShowBackToTop(scrollTop > threshold) + } + + element.addEventListener("scroll", handleScroll, { passive: true }) + return () => element.removeEventListener("scroll", handleScroll) + }, [threshold, elementRef]) + + function scrollToTop() { + if (elementRef?.current) + elementRef.current.scrollTo({ top: 0, behavior: "smooth" }) + else window.scrollTo({ top: 0, behavior: "smooth" }) + } + + return { showBackToTop, scrollToTop } +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index ab139c66b..6021dd963 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -13,6 +13,7 @@ "Accept new price": "Accepter ny pris", "Accessibility": "Tilgængelighed", "Accessible Room": "Tilgængelighedsrum", + "Active": "Aktiv", "Activities": "Aktiviteter", "Add Room": "Tilføj værelse", "Add code": "Tilføj kode", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index fad304104..a7d4bec54 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -13,6 +13,7 @@ "Accept new price": "Neuen Preis akzeptieren", "Accessibility": "Zugänglichkeit", "Accessible Room": "Barrierefreies Zimmer", + "Active": "Aktiv", "Activities": "Aktivitäten", "Add Room": "Zimmer hinzufügen", "Add code": "Code hinzufügen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index b04236e5d..4aa97e93d 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -13,6 +13,7 @@ "Accept new price": "Accept new price", "Accessibility": "Accessibility", "Accessible Room": "Accessibility room", + "Active": "Active", "Activities": "Activities", "Add Room": "Add room", "Add code": "Add code", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 0b5230cf9..e96377e7c 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -13,6 +13,7 @@ "Accept new price": "Hyväksy uusi hinta", "Accessibility": "Saavutettavuus", "Accessible Room": "Esteetön huone", + "Active": "Aktiivinen", "Activities": "Aktiviteetit", "Add Room": "Lisää huone", "Add code": "Lisää koodi", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 2ad4b66b0..0c71eb088 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -13,6 +13,7 @@ "Accept new price": "Aksepterer ny pris", "Accessibility": "Tilgjengelighet", "Accessible Room": "Tilgjengelighetsrom", + "Active": "Aktiv", "Activities": "Aktiviteter", "Add Room": "Legg til rom", "Add code": "Legg til kode", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 540211cdb..6863d28c7 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -13,6 +13,7 @@ "Accept new price": "Accepter ny pris", "Accessibility": "Tillgänglighet", "Accessible Room": "Tillgänglighetsrum", + "Active": "Aktiv", "Activities": "Aktiviteter", "Add Room": "Lägg till rum", "Add code": "Lägg till kod", diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 8a84c6d4a..22d1ac385 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -148,3 +148,9 @@ export const getCityCoordinates = cache( return serverClient().hotel.map.city(input) } ) + +export const getCurrentRewards = cache( + async function getMemoizedCurrentRewards() { + return serverClient().contentstack.rewards.current() + } +) diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts index 05547ca00..f3a38b11b 100644 --- a/server/routers/contentstack/reward/output.ts +++ b/server/routers/contentstack/reward/output.ts @@ -161,10 +161,14 @@ export type CMSRewardWithRedeem = z.output< export type Reward = CMSReward & { id: string | undefined + rewardType: string | undefined + redeemLocation: string | undefined } export type RewardWithRedeem = CMSRewardWithRedeem & { id: string | undefined + rewardType: string | undefined + redeemLocation: string | undefined } // New endpoint related types and schemas. @@ -172,10 +176,11 @@ export type RewardWithRedeem = CMSRewardWithRedeem & { const BenefitReward = z.object({ title: z.string().optional(), id: z.string().optional(), - status: z.string().optional(), + redeemLocation: z.string().optional(), rewardId: z.string().optional(), rewardType: z.string().optional(), rewardTierLevel: z.string().optional(), + status: z.string().optional(), }) const CouponState = z.enum(["claimed", "redeemed", "viewed"]) @@ -191,6 +196,7 @@ const CouponReward = z.object({ id: z.string().optional(), rewardId: z.string().optional(), rewardType: z.string().optional(), + redeemLocation: z.string().optional(), status: z.string().optional(), coupon: z.array(CouponData).optional(), }) @@ -224,3 +230,7 @@ export const validateApiAllTiersSchema = z.record( }), z.array(BenefitReward) ) + +export type RedeemLocation = "Non-redeemable" | "On-site" | "Online" + +export type RewardType = "Tier" | "Member-voucher" | "Surprise" | "Campaign" diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 16956c548..3a3165aaa 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -16,9 +16,10 @@ import { rewardsUpdateInput, } from "./input" import { -type - Reward, validateApiRewardSchema, - validateCategorizedRewardsSchema} from "./output" + type Reward, + validateApiRewardSchema, + validateCategorizedRewardsSchema, +} from "./output" import { getAllCachedApiRewards, getAllRewardCounter, @@ -41,7 +42,6 @@ import { getUnwrapSurpriseSuccessCounter, } from "./utils" - const ONE_HOUR = 60 * 60 export const rewardQueryRouter = router({ @@ -245,13 +245,17 @@ export const rewardQueryRouter = router({ .map(({ rewardId }) => rewardId) const rewards = cmsRewards - .filter((reward) => !wrappedSurprisesIds.includes(reward.reward_id)) - .map((reward) => { + .filter((cmsReward) => !wrappedSurprisesIds.includes(cmsReward.reward_id)) + .map((cmsReward) => { + const apiReward = validatedApiRewards.data.find( + ({ rewardId }) => rewardId === cmsReward.reward_id + ) + return { - ...reward, - id: validatedApiRewards.data.find( - ({ rewardId }) => rewardId === reward.reward_id - )?.id, + ...cmsReward, + id: apiReward?.id, + rewardType: apiReward?.rewardType, + redeemLocation: apiReward?.redeemLocation, } }) @@ -364,6 +368,8 @@ export const rewardQueryRouter = router({ return { ...reward, id: surprise.id, + rewardType: surprise.rewardType, + redeemLocation: surprise.redeemLocation, coupons: "coupon" in surprise ? surprise.coupon || [] : [], } }) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index f69c58d08..1bffd3543 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -105,8 +105,18 @@ const hotelContentSchema = z.object({ imageSizes: imageSizesSchema, }) .default({ - metaData: { title: "", altText: "", altText_En: "", copyRight: "" }, - imageSizes: { tiny: "", small: "", medium: "", large: "" }, + metaData: { + title: "default image", + altText: "default image", + altText_En: "default image", + copyRight: "default image", + }, + imageSizes: { + tiny: "https://placehold.co/1280x720", + small: "https://placehold.co/1280x720", + medium: "https://placehold.co/1280x720", + large: "https://placehold.co/1280x720", + }, }), texts: z.object({ facilityInformation: z.string().optional(), diff --git a/stores/enter-details/index.ts b/stores/enter-details/index.ts index a469eb4f0..b2480471b 100644 --- a/stores/enter-details/index.ts +++ b/stores/enter-details/index.ts @@ -133,7 +133,7 @@ export function createDetailsStore( const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep - navigate(nextStep, searchParams) + navigate(nextStep, state.searchParamString) }) ) }, @@ -141,7 +141,7 @@ export function createDetailsStore( return set( produce((state) => { state.currentStep = step - navigate(step, searchParams) + navigate(step, state.searchParamString) }) ) }, @@ -185,7 +185,7 @@ export function createDetailsStore( const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep - navigate(nextStep, searchParams) + navigate(nextStep, state.searchParamString) }) ) }, @@ -266,7 +266,7 @@ export function createDetailsStore( const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep - navigate(nextStep, searchParams) + navigate(nextStep, state.searchParamString) }) ) }, @@ -304,11 +304,19 @@ export function createDetailsStore( const currentStepIndex = state.steps.indexOf(state.currentStep) const nextStep = state.steps[currentStepIndex + 1] state.currentStep = nextStep - navigate(nextStep, searchParams) + navigate(nextStep, state.searchParamString) + }) + ) + }, + updateSeachParamString(searchParamString) { + return set( + produce((state: DetailsState) => { + state.searchParamString = searchParamString }) ) }, }, + searchParamString: searchParams, bedType: initialState.bedType ?? undefined, booking: initialState.booking, breakfast: diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index c78b5c90e..12760e765 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -5,7 +5,7 @@ interface Room { adults: number roomTypeCode: string rateCode: string - counterRateCode: string + counterRateCode?: string children?: Child[] packages?: RoomPackageCodeEnum[] } diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index 81b7b55af..f01e5e7ea 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -46,10 +46,10 @@ export interface SelectHotelProps { lang: Lang } reservationParams: { - selectHotelParams: URLSearchParams + selectHotelParams: URLSearchParams | undefined searchParams: SelectHotelSearchParams - adultsParams: number - childrenParams: string | undefined - child: Child[] | undefined + adultsInRoom: number + childrenInRoom: string | undefined + childrenInRoomArray: Child[] | undefined } } diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 15f50da7c..164222a88 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -1,6 +1,5 @@ -import { PaymentMethodEnum } from "@/constants/booking" - -import { CreditCard, SafeUser } from "@/types/user" +import type { CreditCard, SafeUser } from "@/types/user" +import type { PaymentMethodEnum } from "@/constants/booking" export interface SectionProps { nextPath: string diff --git a/types/components/myPages/myPage/accountPage.ts b/types/components/myPages/myPage/accountPage.ts index 10690562d..e5133c758 100644 --- a/types/components/myPages/myPage/accountPage.ts +++ b/types/components/myPages/myPage/accountPage.ts @@ -26,10 +26,12 @@ export interface CurrentRewardsClientProps { rewards: (Reward | RewardWithRedeem)[] pageSize: number showRedeem: boolean + membershipNumber?: string | null } export interface RedeemProps { reward: RewardWithRedeem + membershipNumber?: string | null } export type RedeemModalState = "unmounted" | "hidden" | "visible" diff --git a/types/components/myPages/rewards.ts b/types/components/myPages/rewards.ts index 0598ca7e8..4f67602be 100644 --- a/types/components/myPages/rewards.ts +++ b/types/components/myPages/rewards.ts @@ -1,6 +1,11 @@ import type { IconProps } from "@/types/components/icon" +import type { RESTAURANT_REWARD_IDS, REWARD_IDS } from "@/constants/rewards" export interface RewardIconProps extends IconProps { rewardId: string size?: "small" | "medium" | "large" } + +export type RewardId = (typeof REWARD_IDS)[keyof typeof REWARD_IDS] + +export type RestaurantRewardId = (typeof RESTAURANT_REWARD_IDS)[number] diff --git a/types/enums/rewards.ts b/types/enums/rewards.ts deleted file mode 100644 index 0daf4afed..000000000 --- a/types/enums/rewards.ts +++ /dev/null @@ -1,31 +0,0 @@ -export enum RewardId { - // Food & Beverage - TenPercentFood = "tier_10_percent_food_tier", - TwoForOneBreakfast = "tier_2_for_one_breakfast", - FifteenPercentFood = "tier_15_percent_food", - FreeKidsDrink = "tier_free_kids_drink", - FreeBreakfast = "tier_free_breakfast", - - // Monetary Vouchers - Bonus50SEK = "tier_50_SEK_bonus_voucher", - Bonus75SEK = "tier_75_SEK_bonus_voucher", - Bonus100SEK = "tier_100_SEK_bonus_voucher", - Bonus150SEK = "tier_150_SEK_bonus_voucher", - Bonus200SEK = "tier_200_SEK_bonus_voucher", - - // Hotel Perks - EarlyCheckin = "tier_early_checkin_tier", - LateCheckout = "tier_late_checkout", - FreeUpgrade = "tier_free_upgrade", - RoomGuarantee48H = "tier_48_h_room_guarantee", - // GymAccess = "tier_gym_access", - - // Earning & Points - EarnRate25Percent = "tier_25_percent_earn_rate", - EarnRate50Percent = "tier_50_percent_earn_rate", - StayBoostForKids = "tier_stay_boost_for_kids", - MemberRate = "tier_member_rate", - - // Special - YearlyExclusiveGift = "tier_yearly_exclusive_gift", -} diff --git a/types/stores/enter-details.ts b/types/stores/enter-details.ts index ec0fb6ad1..e507ab146 100644 --- a/types/stores/enter-details.ts +++ b/types/stores/enter-details.ts @@ -37,6 +37,7 @@ export interface DetailsState { updateBedType: (data: BedTypeSchema) => void updateBreakfast: (data: BreakfastPackage | false) => void updateDetails: (data: DetailsSchema) => void + updateSeachParamString: (searchParamString: string) => void } bedType: BedTypeSchema | undefined booking: BookingData @@ -52,6 +53,7 @@ export interface DetailsState { roomPrice: Price steps: StepEnum[] totalPrice: Price + searchParamString: string } export type InitialState = Pick<DetailsState, "booking" | "packages"> & diff --git a/utils/rewards.ts b/utils/rewards.ts index 9bf5940a5..272ce3954 100644 --- a/utils/rewards.ts +++ b/utils/rewards.ts @@ -1,5 +1,38 @@ -import { RewardId } from "@/types/enums/rewards" +import { RESTAURANT_REWARD_IDS, REWARD_IDS } from "@/constants/rewards" + +import type { + RestaurantRewardId, + RewardId, +} from "@/types/components/myPages/rewards" +import type { Reward } from "@/server/routers/contentstack/reward/output" export function isValidRewardId(id: string): id is RewardId { - return Object.values<string>(RewardId).includes(id) + return Object.values<string>(REWARD_IDS).includes(id) +} + +export function isRestaurantReward( + rewardId: string +): rewardId is RestaurantRewardId { + return RESTAURANT_REWARD_IDS.some((id) => id === rewardId) +} + +export function redeemLocationIsOnSite( + location: Reward["redeemLocation"] +): location is "On-site" { + return location === "On-site" +} + +export function isTierType(type: Reward["rewardType"]): type is "Tier" { + return type === "Tier" +} + +export function isOnSiteTierReward(reward: Reward): boolean { + return ( + redeemLocationIsOnSite(reward.redeemLocation) && + isTierType(reward.rewardType) + ) +} + +export function isRestaurantOnSiteTierReward(reward: Reward): boolean { + return isOnSiteTierReward(reward) && isRestaurantReward(reward.reward_id) }