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 Surprises from "@/components/MyPages/Surprises"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
@@ -17,6 +18,7 @@ export default async function MyPagesLayout({
|
||||
{children}
|
||||
</section>
|
||||
</section>
|
||||
<Surprises />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"use 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 Grids from "@/components/TempDesignSystem/Grids"
|
||||
@@ -9,15 +12,12 @@ import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Surprises from "../Surprises"
|
||||
|
||||
import styles from "./current.module.css"
|
||||
|
||||
import type { CurrentRewardsClientProps } from "@/types/components/blocks/currentRewards"
|
||||
|
||||
export default function ClientCurrentRewards({
|
||||
initialCurrentRewards,
|
||||
membershipNumber,
|
||||
}: CurrentRewardsClientProps) {
|
||||
const lang = useLang()
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
||||
@@ -39,18 +39,12 @@ export default function ClientCurrentRewards({
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
const rewards =
|
||||
const cmsRewards =
|
||||
data?.pages
|
||||
.flatMap((page) => page?.rewards)
|
||||
.filter((reward): reward is Reward => !!reward) ?? []
|
||||
|
||||
const surprises =
|
||||
data?.pages
|
||||
.flatMap((page) => page?.apiRewards)
|
||||
.filter((reward): reward is ApiReward => reward?.type === "surprise") ??
|
||||
[]
|
||||
|
||||
if (!rewards.length && !surprises.length) {
|
||||
if (!cmsRewards.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -63,7 +57,7 @@ export default function ClientCurrentRewards({
|
||||
return (
|
||||
<>
|
||||
<Grids.Stackable>
|
||||
{rewards.map((reward, idx) => (
|
||||
{cmsRewards.map((reward, idx) => (
|
||||
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
||||
<Title
|
||||
as="h4"
|
||||
@@ -82,7 +76,6 @@ export default function ClientCurrentRewards({
|
||||
) : (
|
||||
<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 SectionContainer from "@/components/Section/Container"
|
||||
@@ -23,19 +22,10 @@ export default async function CurrentRewardsBlock({
|
||||
return null
|
||||
}
|
||||
|
||||
const user = await getProfile()
|
||||
|
||||
if (!user || "error" in user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<SectionHeader title={title} link={link} preamble={subtitle} />
|
||||
<ClientCurrentRewards
|
||||
initialCurrentRewards={initialCurrentRewards}
|
||||
membershipNumber={user.membership?.membershipNumber}
|
||||
/>
|
||||
<ClientCurrentRewards initialCurrentRewards={initialCurrentRewards} />
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
</SectionContainer>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { benefits } from "@/constants/routes/myPages"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
@@ -22,7 +23,7 @@ import styles from "./surprises.module.css"
|
||||
|
||||
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Surprises({
|
||||
export default function SurprisesNotification({
|
||||
surprises,
|
||||
membershipNumber,
|
||||
}: SurprisesProps) {
|
||||
@@ -45,8 +46,8 @@ export default function Surprises({
|
||||
}
|
||||
|
||||
function viewRewards() {
|
||||
if (surprise.id) {
|
||||
update.mutate({ id: surprise.id })
|
||||
if (surprise.reward_id) {
|
||||
update.mutate({ id: surprise.reward_id })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,13 +106,18 @@ export default function Surprises({
|
||||
{showSurprises ? (
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<Surprise title={surprise.title}>
|
||||
<Surprise title={surprise.label}>
|
||||
<Body textAlign="center">
|
||||
This is just some dummy text describing the gift and
|
||||
should be replaced.
|
||||
</Body>
|
||||
<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>
|
||||
{intl.formatMessage({ id: "Membership ID" })}{" "}
|
||||
{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}
|
||||
<Blocks content={accountPage.content} />
|
||||
</MaxWidth>
|
||||
|
||||
<TrackingSDK pageData={tracking} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -2,20 +2,52 @@ import { z } from "zod"
|
||||
|
||||
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
|
||||
.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
rewardId: z.string().optional(),
|
||||
redeemLocation: z.string().optional(),
|
||||
autoApplyReward: z.boolean().default(false),
|
||||
rewardType: z.string().optional(),
|
||||
rewardTierLevel: z.string().optional(),
|
||||
})
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
type: z.literal("custom"),
|
||||
status: z.string().optional(),
|
||||
rewardId: z.string().optional(),
|
||||
redeemLocation: z.string().optional(),
|
||||
autoApplyReward: z.boolean().default(false),
|
||||
rewardType: z.string().optional(),
|
||||
rewardTierLevel: z.string().optional(),
|
||||
}),
|
||||
SurpriseReward,
|
||||
])
|
||||
),
|
||||
})
|
||||
.transform((data) => data.data)
|
||||
@@ -77,6 +109,8 @@ export const validateCmsRewardsSchema = z
|
||||
|
||||
export type ApiReward = z.output<typeof validateApiRewardSchema>[0]
|
||||
|
||||
export type SurpriseReward = z.output<typeof SurpriseReward>
|
||||
|
||||
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
|
||||
|
||||
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]
|
||||
|
||||
@@ -24,11 +24,14 @@ import {
|
||||
import {
|
||||
CmsRewardsResponse,
|
||||
Reward,
|
||||
SurpriseReward,
|
||||
validateApiRewardSchema,
|
||||
validateApiTierRewardsSchema,
|
||||
validateCmsRewardsSchema,
|
||||
} from "./output"
|
||||
|
||||
import { Surprise } from "@/types/components/blocks/surprises"
|
||||
|
||||
const meter = metrics.getMeter("trpc.reward")
|
||||
// OpenTelemetry metrics: Reward
|
||||
|
||||
@@ -259,36 +262,21 @@ export const rewardQueryRouter = router({
|
||||
const nextCursor =
|
||||
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)
|
||||
|
||||
return {
|
||||
rewards: cmsRewards,
|
||||
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",
|
||||
},
|
||||
]),
|
||||
rewards,
|
||||
nextCursor,
|
||||
}
|
||||
}),
|
||||
@@ -402,6 +390,99 @@ export const rewardQueryRouter = router({
|
||||
getAllRewardSuccessCounter.add(1)
|
||||
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
|
||||
.input(rewardsUpdateInput)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -5,8 +5,6 @@ import { SafeUser } from "@/types/user"
|
||||
export type CurrentRewardsClientProps = {
|
||||
initialCurrentRewards: {
|
||||
rewards: Reward[]
|
||||
apiRewards: ApiReward[]
|
||||
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 {
|
||||
surprises: ApiReward[]
|
||||
surprises: Surprise[]
|
||||
membershipNumber?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user