Feat/BOOK-61 refactor hotel page css variables * feat(BOOK-61): Breadcrumbs * feat(BOOK-61): intro section * feat(BOOK-61): show more button * feat(BOOK-61): rooms section * feat(BOOK-61): sidepeeks * feat(BOOK-61): deprecated old Link component * feat(BOOK-61): added new TextLink component to the design-system * feat(BOOK-61): replaced deprecated links with new TextLink component * feat(BOOK-61): miscellaneous changes Approved-by: Bianca Widstam Approved-by: Christel Westerberg
281 lines
8.2 KiB
TypeScript
281 lines
8.2 KiB
TypeScript
"use client"
|
|
|
|
import { AnimatePresence, motion } from "motion/react"
|
|
import { usePathname } from "next/navigation"
|
|
import { useState } from "react"
|
|
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { customerService } from "@scandic-hotels/common/constants/routes/customerService"
|
|
import { benefits } from "@scandic-hotels/common/constants/routes/myPages"
|
|
import { logger } from "@scandic-hotels/common/logger"
|
|
import Link from "@scandic-hotels/design-system/OldDSLink"
|
|
import { toast } from "@scandic-hotels/design-system/Toast"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
import { trpc } from "@scandic-hotels/trpc/client"
|
|
|
|
import {
|
|
benefits as webviewBenefits,
|
|
myPagesWebviews,
|
|
} from "@/constants/routes/webviews"
|
|
|
|
import useLang from "@/hooks/useLang"
|
|
|
|
import confetti from "./confetti"
|
|
import Header from "./Header"
|
|
import Initial from "./Initial"
|
|
import Navigation from "./Navigation"
|
|
import Slide from "./Slide"
|
|
|
|
import styles from "./surprises.module.css"
|
|
|
|
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
|
|
|
const MotionModal = motion.create(Modal)
|
|
|
|
export default function SurprisesNotification({
|
|
surprises: initialData,
|
|
}: SurprisesProps) {
|
|
const lang = useLang()
|
|
const intl = useIntl()
|
|
const pathname = usePathname()
|
|
const [open, setOpen] = useState(true)
|
|
const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0])
|
|
const [showSurprises, setShowSurprises] = useState(false)
|
|
const utils = trpc.useUtils()
|
|
|
|
const { data } = trpc.contentstack.rewards.surprises.useQuery(
|
|
{
|
|
lang,
|
|
},
|
|
{
|
|
initialData,
|
|
refetchInterval: 1000 * 60 * 5, // every 5 minutes
|
|
refetchIntervalInBackground: false,
|
|
}
|
|
)
|
|
|
|
const surprises = data ?? []
|
|
|
|
const unwrap = trpc.contentstack.rewards.unwrap.useMutation({
|
|
onSuccess: () => {
|
|
utils.contentstack.rewards.current.invalidate({ lang })
|
|
|
|
const onBenefitsPage = pathname.indexOf(benefits[lang]) === 0
|
|
const onWebviewBenefitsPage =
|
|
pathname.indexOf(webviewBenefits[lang]) === 0
|
|
|
|
if (onBenefitsPage || onWebviewBenefitsPage) {
|
|
return
|
|
}
|
|
|
|
const benefitPageUrl = myPagesWebviews.includes(pathname)
|
|
? webviewBenefits[lang]
|
|
: benefits[lang]
|
|
|
|
toast.success(
|
|
<>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "myPages.countOfGiftsAddedToYourBenefits",
|
|
defaultMessage:
|
|
"{amount, plural, one {Gift} other {Gifts}} added to your benefits",
|
|
},
|
|
{ amount: surprises.length }
|
|
)}
|
|
<br />
|
|
<Link href={benefitPageUrl} textDecoration="underline">
|
|
{intl.formatMessage({
|
|
id: "myPages.goToMyBenfits",
|
|
defaultMessage: "Go to My Benefits",
|
|
})}
|
|
</Link>
|
|
</>
|
|
)
|
|
},
|
|
onError: (error) => {
|
|
logger.error("Failed to unwrap surprise", error)
|
|
toast.error(
|
|
<>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "myPages.somethingWentWrongShowingYourSurprise",
|
|
defaultMessage:
|
|
"Oops! Something went wrong while showing your surprise. Please refresh the page or try again later. If the issue persists, <link>contact the support.</link>",
|
|
},
|
|
{
|
|
link: (str) => (
|
|
<Link textDecoration="underline" href={customerService[lang]}>
|
|
{str}
|
|
</Link>
|
|
),
|
|
}
|
|
)}
|
|
</>
|
|
)
|
|
},
|
|
})
|
|
|
|
const totalSurprises = surprises.length
|
|
if (!totalSurprises) {
|
|
return null
|
|
}
|
|
|
|
const surprise = surprises[selectedSurprise]
|
|
|
|
function showSurprise(newDirection: number) {
|
|
setSelectedSurprise(([currentIndex]) => [
|
|
currentIndex + newDirection,
|
|
newDirection,
|
|
])
|
|
}
|
|
|
|
async function viewRewards() {
|
|
const updates = surprises
|
|
.map((surprise) => {
|
|
const coupons = surprise.coupon
|
|
.map((coupon) => {
|
|
if (coupon.couponCode) {
|
|
return {
|
|
rewardId: surprise.id,
|
|
couponCode: coupon.couponCode,
|
|
}
|
|
}
|
|
})
|
|
.filter(
|
|
(coupon): coupon is { rewardId: string; couponCode: string } =>
|
|
!!coupon
|
|
)
|
|
|
|
return coupons
|
|
})
|
|
.flat()
|
|
|
|
unwrap.mutate(updates)
|
|
}
|
|
|
|
return (
|
|
<ModalOverlay
|
|
className={styles.overlay}
|
|
isOpen={open}
|
|
onOpenChange={setOpen}
|
|
isKeyboardDismissDisabled
|
|
>
|
|
<canvas id="surprise-confetti" className={styles.confetti} />
|
|
<AnimatePresence mode="wait">
|
|
{open && (
|
|
<MotionModal
|
|
className={styles.modal}
|
|
initial={{ y: 32, opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: 32, opacity: 0 }}
|
|
transition={{
|
|
y: { duration: 0.4, ease: "easeInOut" },
|
|
opacity: { duration: 0.4, ease: "easeInOut" },
|
|
}}
|
|
onAnimationComplete={confetti}
|
|
>
|
|
<Dialog
|
|
aria-label={intl.formatMessage({
|
|
id: "myPages.Surprises",
|
|
defaultMessage: "Surprises",
|
|
})}
|
|
className={styles.dialog}
|
|
>
|
|
{({ close }) => {
|
|
return (
|
|
<>
|
|
<Header
|
|
onClose={() => {
|
|
viewRewards()
|
|
close()
|
|
}}
|
|
>
|
|
{showSurprises && totalSurprises > 1 && (
|
|
<Typography variant="Tag/sm">
|
|
<p>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "myPages.AmountOutOfTotalWithValues",
|
|
defaultMessage: "{amount} out of {total}",
|
|
},
|
|
{
|
|
amount: selectedSurprise + 1,
|
|
total: totalSurprises,
|
|
}
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
)}
|
|
</Header>
|
|
|
|
{showSurprises ? (
|
|
<>
|
|
<AnimatePresence
|
|
mode="popLayout"
|
|
initial={false}
|
|
custom={direction}
|
|
>
|
|
<motion.div
|
|
key={selectedSurprise}
|
|
custom={direction}
|
|
variants={variants}
|
|
initial="enter"
|
|
animate="center"
|
|
exit="exit"
|
|
transition={{
|
|
x: { type: "ease", duration: 0.5 },
|
|
opacity: { duration: 0.2 },
|
|
}}
|
|
layout
|
|
>
|
|
<Slide surprise={surprise} />
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
|
|
{totalSurprises > 1 && (
|
|
<Navigation
|
|
selectedSurprise={selectedSurprise}
|
|
totalSurprises={totalSurprises}
|
|
showSurprise={showSurprise}
|
|
/>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Initial
|
|
totalSurprises={totalSurprises}
|
|
onOpen={() => {
|
|
setShowSurprises(true)
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}}
|
|
</Dialog>
|
|
</MotionModal>
|
|
)}
|
|
</AnimatePresence>
|
|
</ModalOverlay>
|
|
)
|
|
}
|
|
|
|
const variants = {
|
|
enter: (direction: number) => {
|
|
return {
|
|
x: direction > 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
}
|
|
},
|
|
center: {
|
|
x: 0,
|
|
opacity: 1,
|
|
},
|
|
exit: (direction: number) => {
|
|
return {
|
|
x: direction < 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
}
|
|
},
|
|
}
|