Files
web/apps/scandic-web/components/MyPages/Surprises/Client.tsx
Rasmus Langvad c65091b36a Merged in feat/SW-3644-storybook-v10 (pull request #3240)
feat(SW-3644): Storybook v10

* Auto update to Storybook v10

* Add scandic theme and logo

* Update yarn.lock

* Update formatting of package.json

* Update vitest config and playwright plugin

* Remove vitest 4 update

* Re-added comment

* Update the Typography component to explicitly return React.ReactNode

* Add an explicit type assertion to the export

* Add an explicit type assertion to the export for Checkbox

* Explicit return type assertion

* Add an explicit type assertion to the export

* Update @types/react and fix ts warnings

* Updated typings


Approved-by: Linus Flood
Approved-by: Matilda Landström
2025-11-28 08:05:40 +00:00

285 lines
8.4 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: "tween",
ease: "easeInOut",
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,
}
},
} as const