fix(SW-556): now fetching surprises separately in component.
also showing surprises on any account page
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import Sidebar from "@/components/MyPages/Sidebar"
|
import Sidebar from "@/components/MyPages/Sidebar"
|
||||||
|
import Surprises from "@/components/MyPages/Surprises"
|
||||||
|
|
||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export default async function MyPagesLayout({
|
|||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
<Surprises />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
import { ApiReward, Reward } from "@/server/routers/contentstack/reward/output"
|
import {
|
||||||
|
Reward,
|
||||||
|
SurpriseReward,
|
||||||
|
} from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
import Grids from "@/components/TempDesignSystem/Grids"
|
||||||
@@ -9,15 +12,12 @@ import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
|||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import Surprises from "../Surprises"
|
|
||||||
|
|
||||||
import styles from "./current.module.css"
|
import styles from "./current.module.css"
|
||||||
|
|
||||||
import type { CurrentRewardsClientProps } from "@/types/components/blocks/currentRewards"
|
import type { CurrentRewardsClientProps } from "@/types/components/blocks/currentRewards"
|
||||||
|
|
||||||
export default function ClientCurrentRewards({
|
export default function ClientCurrentRewards({
|
||||||
initialCurrentRewards,
|
initialCurrentRewards,
|
||||||
membershipNumber,
|
|
||||||
}: CurrentRewardsClientProps) {
|
}: CurrentRewardsClientProps) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
||||||
@@ -39,18 +39,12 @@ export default function ClientCurrentRewards({
|
|||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
|
|
||||||
const rewards =
|
const cmsRewards =
|
||||||
data?.pages
|
data?.pages
|
||||||
.flatMap((page) => page?.rewards)
|
.flatMap((page) => page?.rewards)
|
||||||
.filter((reward): reward is Reward => !!reward) ?? []
|
.filter((reward): reward is Reward => !!reward) ?? []
|
||||||
|
|
||||||
const surprises =
|
if (!cmsRewards.length) {
|
||||||
data?.pages
|
|
||||||
.flatMap((page) => page?.apiRewards)
|
|
||||||
.filter((reward): reward is ApiReward => reward?.type === "surprise") ??
|
|
||||||
[]
|
|
||||||
|
|
||||||
if (!rewards.length && !surprises.length) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +57,7 @@ export default function ClientCurrentRewards({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grids.Stackable>
|
<Grids.Stackable>
|
||||||
{rewards.map((reward, idx) => (
|
{cmsRewards.map((reward, idx) => (
|
||||||
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
||||||
<Title
|
<Title
|
||||||
as="h4"
|
as="h4"
|
||||||
@@ -82,7 +76,6 @@ export default function ClientCurrentRewards({
|
|||||||
) : (
|
) : (
|
||||||
<ShowMoreButton loadMoreData={loadMoreData} />
|
<ShowMoreButton loadMoreData={loadMoreData} />
|
||||||
))}
|
))}
|
||||||
<Surprises surprises={surprises} membershipNumber={membershipNumber} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import SectionContainer from "@/components/Section/Container"
|
import SectionContainer from "@/components/Section/Container"
|
||||||
@@ -23,19 +22,10 @@ export default async function CurrentRewardsBlock({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getProfile()
|
|
||||||
|
|
||||||
if (!user || "error" in user) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<SectionHeader title={title} link={link} preamble={subtitle} />
|
<SectionHeader title={title} link={link} preamble={subtitle} />
|
||||||
<ClientCurrentRewards
|
<ClientCurrentRewards initialCurrentRewards={initialCurrentRewards} />
|
||||||
initialCurrentRewards={initialCurrentRewards}
|
|
||||||
membershipNumber={user.membership?.membershipNumber}
|
|
||||||
/>
|
|
||||||
<SectionLink link={link} variant="mobile" />
|
<SectionLink link={link} variant="mobile" />
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { benefits } from "@/constants/routes/myPages"
|
import { benefits } from "@/constants/routes/myPages"
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons"
|
||||||
@@ -22,7 +23,7 @@ import styles from "./surprises.module.css"
|
|||||||
|
|
||||||
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
||||||
|
|
||||||
export default function Surprises({
|
export default function SurprisesNotification({
|
||||||
surprises,
|
surprises,
|
||||||
membershipNumber,
|
membershipNumber,
|
||||||
}: SurprisesProps) {
|
}: SurprisesProps) {
|
||||||
@@ -45,8 +46,8 @@ export default function Surprises({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function viewRewards() {
|
function viewRewards() {
|
||||||
if (surprise.id) {
|
if (surprise.reward_id) {
|
||||||
update.mutate({ id: surprise.id })
|
update.mutate({ id: surprise.reward_id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +106,18 @@ export default function Surprises({
|
|||||||
{showSurprises ? (
|
{showSurprises ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Surprise title={surprise.title}>
|
<Surprise title={surprise.label}>
|
||||||
<Body textAlign="center">
|
<Body textAlign="center">
|
||||||
This is just some dummy text describing the gift and
|
This is just some dummy text describing the gift and
|
||||||
should be replaced.
|
should be replaced.
|
||||||
</Body>
|
</Body>
|
||||||
<div className={styles.badge}>
|
<div className={styles.badge}>
|
||||||
<Caption>Valid through DD M YYYY</Caption>
|
<Caption>
|
||||||
|
Valid through{" "}
|
||||||
|
{dt(surprise.endsAt)
|
||||||
|
.locale(lang)
|
||||||
|
.format("DD MMM YYYY")}
|
||||||
|
</Caption>
|
||||||
<Caption>
|
<Caption>
|
||||||
{intl.formatMessage({ id: "Membership ID" })}{" "}
|
{intl.formatMessage({ id: "Membership ID" })}{" "}
|
||||||
{membershipNumber}
|
{membershipNumber}
|
||||||
25
components/MyPages/Surprises/index.tsx
Normal file
25
components/MyPages/Surprises/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||||
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
|
import SurprisesNotification from "./SurprisesNotification"
|
||||||
|
|
||||||
|
export default async function Surprises() {
|
||||||
|
const user = await getProfile()
|
||||||
|
|
||||||
|
if (!user || "error" in user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const surprises = await serverClient().contentstack.rewards.surprises()
|
||||||
|
|
||||||
|
if (!surprises) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SurprisesNotification
|
||||||
|
surprises={surprises}
|
||||||
|
membershipNumber={user.membership?.membershipNumber}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,7 +30,6 @@ export default async function AccountPage() {
|
|||||||
{linkToOverview ? <LinkToOverview /> : null}
|
{linkToOverview ? <LinkToOverview /> : null}
|
||||||
<Blocks content={accountPage.content} />
|
<Blocks content={accountPage.content} />
|
||||||
</MaxWidth>
|
</MaxWidth>
|
||||||
|
|
||||||
<TrackingSDK pageData={tracking} />
|
<TrackingSDK pageData={tracking} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,20 +2,52 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||||
|
|
||||||
|
const Coupon = z.object({
|
||||||
|
code: z.string().optional(),
|
||||||
|
status: z.string().optional(),
|
||||||
|
createdAt: z.string().datetime({ offset: true }).optional(),
|
||||||
|
customer: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
}),
|
||||||
|
name: z.string().optional(),
|
||||||
|
claimedAt: z.string().datetime({ offset: true }).optional(),
|
||||||
|
// redeemedAt: z.string().datetime({ offset: true }).optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
value: z.number().optional(),
|
||||||
|
pool: z.string().optional(),
|
||||||
|
cfUnwrapped: z.boolean().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
const SurpriseReward = z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
id: z.string().optional(),
|
||||||
|
type: z.literal("coupon"),
|
||||||
|
status: z.string().optional(),
|
||||||
|
rewardId: z.string().optional(),
|
||||||
|
redeemLocation: z.string().optional(),
|
||||||
|
autoApplyReward: z.boolean().default(false),
|
||||||
|
rewardType: z.string().optional(),
|
||||||
|
endsAt: z.string().datetime({ offset: true }).optional(),
|
||||||
|
coupons: z.array(Coupon).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const validateApiRewardSchema = z
|
export const validateApiRewardSchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
z.object({
|
z.discriminatedUnion("type", [
|
||||||
title: z.string().optional(),
|
z.object({
|
||||||
id: z.string().optional(),
|
title: z.string().optional(),
|
||||||
type: z.string().optional(),
|
id: z.string().optional(),
|
||||||
status: z.string().optional(),
|
type: z.literal("custom"),
|
||||||
rewardId: z.string().optional(),
|
status: z.string().optional(),
|
||||||
redeemLocation: z.string().optional(),
|
rewardId: z.string().optional(),
|
||||||
autoApplyReward: z.boolean().default(false),
|
redeemLocation: z.string().optional(),
|
||||||
rewardType: z.string().optional(),
|
autoApplyReward: z.boolean().default(false),
|
||||||
rewardTierLevel: z.string().optional(),
|
rewardType: z.string().optional(),
|
||||||
})
|
rewardTierLevel: z.string().optional(),
|
||||||
|
}),
|
||||||
|
SurpriseReward,
|
||||||
|
])
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.transform((data) => data.data)
|
.transform((data) => data.data)
|
||||||
@@ -77,6 +109,8 @@ export const validateCmsRewardsSchema = z
|
|||||||
|
|
||||||
export type ApiReward = z.output<typeof validateApiRewardSchema>[0]
|
export type ApiReward = z.output<typeof validateApiRewardSchema>[0]
|
||||||
|
|
||||||
|
export type SurpriseReward = z.output<typeof SurpriseReward>
|
||||||
|
|
||||||
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
|
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
|
||||||
|
|
||||||
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]
|
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
CmsRewardsResponse,
|
CmsRewardsResponse,
|
||||||
Reward,
|
Reward,
|
||||||
|
SurpriseReward,
|
||||||
validateApiRewardSchema,
|
validateApiRewardSchema,
|
||||||
validateApiTierRewardsSchema,
|
validateApiTierRewardsSchema,
|
||||||
validateCmsRewardsSchema,
|
validateCmsRewardsSchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
|
|
||||||
|
import { Surprise } from "@/types/components/blocks/surprises"
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.reward")
|
const meter = metrics.getMeter("trpc.reward")
|
||||||
// OpenTelemetry metrics: Reward
|
// OpenTelemetry metrics: Reward
|
||||||
|
|
||||||
@@ -259,36 +262,21 @@ export const rewardQueryRouter = router({
|
|||||||
const nextCursor =
|
const nextCursor =
|
||||||
limit + cursor < rewardIds.length ? limit + cursor : undefined
|
limit + cursor < rewardIds.length ? limit + cursor : undefined
|
||||||
|
|
||||||
|
const surprisesIds = validatedApiRewards.data
|
||||||
|
.filter(
|
||||||
|
({ type, rewardType }) =>
|
||||||
|
type === "coupon" && rewardType === "Surprise"
|
||||||
|
)
|
||||||
|
.map(({ rewardId }) => rewardId)
|
||||||
|
|
||||||
|
const rewards = cmsRewards.filter(
|
||||||
|
(reward) => !surprisesIds.includes(reward.reward_id)
|
||||||
|
)
|
||||||
|
|
||||||
getCurrentRewardSuccessCounter.add(1)
|
getCurrentRewardSuccessCounter.add(1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rewards: cmsRewards,
|
rewards,
|
||||||
apiRewards: validatedApiRewards.data
|
|
||||||
// FIXME: Remove these mocks before merging
|
|
||||||
.concat([
|
|
||||||
{
|
|
||||||
autoApplyReward: false,
|
|
||||||
title: "Free kids drink when staying",
|
|
||||||
id: "fake-id",
|
|
||||||
type: "surprise",
|
|
||||||
status: "active",
|
|
||||||
rewardId: "tier_free_kids_drink",
|
|
||||||
redeemLocation: "On-site",
|
|
||||||
rewardType: "Tier",
|
|
||||||
rewardTierLevel: "L1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
autoApplyReward: false,
|
|
||||||
title: "Free kanelbulle",
|
|
||||||
id: "fake-id-2",
|
|
||||||
type: "surprise",
|
|
||||||
status: "active",
|
|
||||||
rewardId: "tier_free_kanelbulle",
|
|
||||||
redeemLocation: "On-site",
|
|
||||||
rewardType: "Tier",
|
|
||||||
rewardTierLevel: "L1",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
nextCursor,
|
nextCursor,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -402,6 +390,99 @@ export const rewardQueryRouter = router({
|
|||||||
getAllRewardSuccessCounter.add(1)
|
getAllRewardSuccessCounter.add(1)
|
||||||
return levelsWithRewards
|
return levelsWithRewards
|
||||||
}),
|
}),
|
||||||
|
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
|
||||||
|
getCurrentRewardCounter.add(1)
|
||||||
|
|
||||||
|
const apiResponse = await api.get(api.endpoints.v1.rewards, {
|
||||||
|
cache: undefined, // override defaultOptions
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||||
|
},
|
||||||
|
next: { revalidate: 60 * 60 },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
getCurrentRewardFailCounter.add(1, {
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.reward error ",
|
||||||
|
JSON.stringify({
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await apiResponse.json()
|
||||||
|
|
||||||
|
const validatedApiRewards = validateApiRewardSchema.safeParse(data)
|
||||||
|
|
||||||
|
if (!validatedApiRewards.success) {
|
||||||
|
getCurrentRewardFailCounter.add(1, {
|
||||||
|
locale: ctx.lang,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validatedApiRewards.error),
|
||||||
|
})
|
||||||
|
console.error(validatedApiRewards.error)
|
||||||
|
console.error(
|
||||||
|
"contentstack.surprises validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { locale: ctx.lang },
|
||||||
|
error: validatedApiRewards.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewardIds = validatedApiRewards.data
|
||||||
|
.map((reward) => reward?.rewardId)
|
||||||
|
.filter((rewardId): rewardId is string => !!rewardId)
|
||||||
|
.sort()
|
||||||
|
|
||||||
|
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||||
|
|
||||||
|
if (!cmsRewards) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentRewardSuccessCounter.add(1)
|
||||||
|
|
||||||
|
const surprises =
|
||||||
|
validatedApiRewards.data
|
||||||
|
.filter(
|
||||||
|
(reward): reward is SurpriseReward =>
|
||||||
|
reward?.type === "coupon" && reward?.rewardType === "Surprise"
|
||||||
|
)
|
||||||
|
.map((surprise) => {
|
||||||
|
const reward = cmsRewards.find(
|
||||||
|
({ reward_id }) => surprise.rewardId === reward_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!reward) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...reward,
|
||||||
|
id: surprise.id,
|
||||||
|
endsAt: surprise.endsAt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((surprise): surprise is Surprise => !!surprise) ?? []
|
||||||
|
|
||||||
|
return surprises
|
||||||
|
}),
|
||||||
update: contentStackBaseWithProtectedProcedure
|
update: contentStackBaseWithProtectedProcedure
|
||||||
.input(rewardsUpdateInput)
|
.input(rewardsUpdateInput)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { SafeUser } from "@/types/user"
|
|||||||
export type CurrentRewardsClientProps = {
|
export type CurrentRewardsClientProps = {
|
||||||
initialCurrentRewards: {
|
initialCurrentRewards: {
|
||||||
rewards: Reward[]
|
rewards: Reward[]
|
||||||
apiRewards: ApiReward[]
|
|
||||||
nextCursor: number | undefined
|
nextCursor: number | undefined
|
||||||
}
|
}
|
||||||
membershipNumber?: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { ApiReward } from "@/server/routers/contentstack/reward/output"
|
import {
|
||||||
|
Reward,
|
||||||
|
SurpriseReward,
|
||||||
|
} from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
|
export interface Surprise extends Reward {
|
||||||
|
endsAt: SurpriseReward["endsAt"]
|
||||||
|
id: SurpriseReward["id"]
|
||||||
|
}
|
||||||
|
|
||||||
export interface SurprisesProps {
|
export interface SurprisesProps {
|
||||||
surprises: ApiReward[]
|
surprises: Surprise[]
|
||||||
membershipNumber?: string
|
membershipNumber?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user