feat(LOY-53): CurrentRewards - replace show more functionality with pagination

This commit is contained in:
Chuma McPhoy
2024-12-02 21:11:53 +01:00
parent 9e42cf0f37
commit 28d4d752e9
11 changed files with 124 additions and 167 deletions

View File

@@ -6,9 +6,9 @@ import { useState } from "react"
import { trpc } from "@/lib/trpc/client" import { trpc } from "@/lib/trpc/client"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import Pagination from "@/components/MyPages/Pagination"
import ClientTable from "./ClientTable" import ClientTable from "./ClientTable"
import Pagination from "./Pagination"
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn" import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"

View File

@@ -1,14 +1,11 @@
"use client" "use client"
import { trpc } from "@/lib/trpc/client" import { useState } from "react"
import { Reward } from "@/server/routers/contentstack/reward/output"
import Image from "@/components/Image" import Image from "@/components/Image"
import LoadingSpinner from "@/components/LoadingSpinner" import Pagination from "@/components/MyPages/Pagination"
import Grids from "@/components/TempDesignSystem/Grids" import Grids from "@/components/TempDesignSystem/Grids"
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 Redeem from "./Redeem" import Redeem from "./Redeem"
@@ -17,46 +14,21 @@ import styles from "./current.module.css"
import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage" import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage"
export default function ClientCurrentRewards({ export default function ClientCurrentRewards({
initialCurrentRewards, rewards,
pageSize,
showRedeem, showRedeem,
}: CurrentRewardsClientProps) { }: CurrentRewardsClientProps) {
const lang = useLang() const [currentPage, setCurrentPage] = useState(1)
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
trpc.contentstack.rewards.current.useInfiniteQuery(
{
limit: 3,
lang,
},
{
getNextPageParam: (lastPage) => lastPage?.nextCursor,
initialData: {
pageParams: [undefined, 1],
pages: [initialCurrentRewards],
},
}
)
function loadMoreData() {
if (hasNextPage) {
fetchNextPage()
}
}
const filteredRewards = data?.pages.filter((page) => page?.rewards) ?? []
const rewards = filteredRewards
.flatMap((page) => page?.rewards)
.filter((reward): reward is Reward => !!reward)
if (isLoading) { const totalPages = Math.ceil(rewards.length / pageSize)
return <LoadingSpinner /> const startIndex = (currentPage - 1) * pageSize
} const endIndex = startIndex + pageSize
const currentRewards = rewards.slice(startIndex, endIndex)
if (!rewards.length) {
return null
}
return ( return (
<div> <div className={styles.container}>
<Grids.Stackable> <Grids.Stackable>
{rewards.map((reward, idx) => ( {currentRewards.map((reward, idx) => (
<article className={styles.card} key={`${reward.reward_id}-${idx}`}> <article className={styles.card} key={`${reward.reward_id}-${idx}`}>
<div className={styles.content}> <div className={styles.content}>
<Image <Image
@@ -82,12 +54,11 @@ export default function ClientCurrentRewards({
</article> </article>
))} ))}
</Grids.Stackable> </Grids.Stackable>
{hasNextPage && <Pagination
(isFetching ? ( pageCount={totalPages}
<LoadingSpinner /> currentPage={currentPage}
) : ( handlePageChange={setCurrentPage}
<ShowMoreButton loadMoreData={loadMoreData} /> />
))}
</div> </div>
) )
} }

View File

@@ -123,3 +123,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x4);
}

View File

@@ -14,12 +14,9 @@ export default async function CurrentRewardsBlock({
subtitle, subtitle,
link, link,
}: AccountPageComponentProps) { }: AccountPageComponentProps) {
const initialCurrentRewards = const rewardsResponse = await serverClient().contentstack.rewards.current()
await serverClient().contentstack.rewards.current({
limit: 3,
})
if (!initialCurrentRewards) { if (!rewardsResponse?.rewards.length) {
return null return null
} }
@@ -27,7 +24,8 @@ export default async function CurrentRewardsBlock({
<SectionContainer> <SectionContainer>
<SectionHeader title={title} link={link} preamble={subtitle} /> <SectionHeader title={title} link={link} preamble={subtitle} />
<ClientCurrentRewards <ClientCurrentRewards
initialCurrentRewards={initialCurrentRewards} rewards={rewardsResponse.rewards}
pageSize={6}
showRedeem={env.USE_NEW_REWARDS_ENDPOINT} showRedeem={env.USE_NEW_REWARDS_ENDPOINT}
/> />
<SectionLink link={link} variant="mobile" /> <SectionLink link={link} variant="mobile" />

View File

@@ -5,7 +5,7 @@ import styles from "./pagination.module.css"
import { import {
PaginationButtonProps, PaginationButtonProps,
PaginationProps, PaginationProps,
} from "@/types/components/myPages/myPage/earnAndBurn" } from "@/types/components/myPages/pagination"
function PaginationButton({ function PaginationButton({
children, children,

View File

@@ -34,7 +34,7 @@
} }
.paginationButtonActive { .paginationButtonActive {
color: var(--WHITE); color: var(--Base-Text-Inverted);
background-color: var(--Base-Text-Accent); background-color: var(--Base-Text-Accent);
border-radius: var(--Corner-radius-Rounded); border-radius: var(--Corner-radius-Rounded);
} }

View File

@@ -1,6 +1,5 @@
import { z } from "zod" import { z } from "zod"
import { Lang } from "@/constants/languages"
import { MembershipLevelEnum } from "@/constants/membershipLevels" import { MembershipLevelEnum } from "@/constants/membershipLevels"
export const rewardsByLevelInput = z.object({ export const rewardsByLevelInput = z.object({
@@ -12,12 +11,6 @@ export const rewardsAllInput = z
.object({ unique: z.boolean() }) .object({ unique: z.boolean() })
.default({ unique: false }) .default({ unique: false })
export const rewardsCurrentInput = z.object({
limit: z.number().min(1).default(3),
cursor: z.number().optional().default(0),
lang: z.nativeEnum(Lang).optional(),
})
export const rewardsUpdateInput = z.array( export const rewardsUpdateInput = z.array(
z.object({ z.object({
rewardId: z.string(), rewardId: z.string(),

View File

@@ -12,7 +12,6 @@ import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import { import {
rewardsAllInput, rewardsAllInput,
rewardsByLevelInput, rewardsByLevelInput,
rewardsCurrentInput,
rewardsRedeemInput, rewardsRedeemInput,
rewardsUpdateInput, rewardsUpdateInput,
} from "./input" } from "./input"
@@ -161,115 +160,105 @@ export const rewardQueryRouter = router({
getByLevelRewardSuccessCounter.add(1) getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}), }),
current: contentStackBaseWithProtectedProcedure current: contentStackBaseWithProtectedProcedure.query(async function ({
.input(rewardsCurrentInput) ctx,
.query(async function ({ input, ctx }) { }) {
getCurrentRewardCounter.add(1) getCurrentRewardCounter.add(1)
const { limit, cursor } = input const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT
const endpoint = isNewEndpoint
? api.endpoints.v1.Profile.Reward.reward
: api.endpoints.v1.Profile.reward
const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT const apiResponse = await api.get(endpoint, {
const endpoint = isNewEndpoint cache: undefined, // override defaultOptions
? api.endpoints.v1.Profile.Reward.reward headers: {
: api.endpoints.v1.Profile.reward Authorization: `Bearer ${ctx.session.token.access_token}`,
},
next: { revalidate: ONE_HOUR },
})
const apiResponse = await api.get(endpoint, { if (!apiResponse.ok) {
cache: undefined, // override defaultOptions const text = await apiResponse.text()
headers: { getCurrentRewardFailCounter.add(1, {
Authorization: `Bearer ${ctx.session.token.access_token}`, error_type: "http_error",
}, error: JSON.stringify({
next: { revalidate: ONE_HOUR }, status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
}) })
console.error(
if (!apiResponse.ok) { "api.reward error ",
const text = await apiResponse.text() JSON.stringify({
getCurrentRewardFailCounter.add(1, { error: {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status, status: apiResponse.status,
statusText: apiResponse.statusText, statusText: apiResponse.statusText,
text, text,
}), },
}) })
console.error( )
"api.reward error ", return null
JSON.stringify({ }
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const data = await apiResponse.json() const data = await apiResponse.json()
const validatedApiRewards = isNewEndpoint const validatedApiRewards = isNewEndpoint
? validateCategorizedRewardsSchema.safeParse(data) ? validateCategorizedRewardsSchema.safeParse(data)
: validateApiRewardSchema.safeParse(data) : validateApiRewardSchema.safeParse(data)
if (!validatedApiRewards.success) { if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, { getCurrentRewardFailCounter.add(1, {
locale: ctx.lang, locale: ctx.lang,
error_type: "validation_error", error_type: "validation_error",
error: JSON.stringify(validatedApiRewards.error), error: JSON.stringify(validatedApiRewards.error),
})
console.error(validatedApiRewards.error)
console.error(
"contentstack.rewards validation error",
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
}) })
console.error(validatedApiRewards.error) )
console.error( return null
"contentstack.rewards validation error", }
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
return null
}
const rewardIds = validatedApiRewards.data const rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId) .map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId) .filter((rewardId): rewardId is string => !!rewardId)
.sort() .sort()
const slicedData = rewardIds.slice(cursor, limit + cursor) const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
const cmsRewards = await getCmsRewards(ctx.lang, slicedData) if (!cmsRewards) {
return null
}
if (!cmsRewards) { const wrappedSurprisesIds = validatedApiRewards.data
return null .filter(
} (reward) =>
reward.type === "coupon" &&
reward.rewardType === "Surprise" &&
"coupon" in reward &&
reward.coupon?.some(({ unwrapped }) => !unwrapped)
)
.map(({ rewardId }) => rewardId)
const nextCursor = const rewards = cmsRewards
limit + cursor < rewardIds.length ? limit + cursor : undefined .filter((reward) => !wrappedSurprisesIds.includes(reward.reward_id))
.map((reward) => {
return {
...reward,
id: validatedApiRewards.data.find(
({ rewardId }) => rewardId === reward.reward_id
)?.id,
}
})
const wrappedSurprisesIds = validatedApiRewards.data getCurrentRewardSuccessCounter.add(1)
.filter(
(reward) =>
reward.type === "coupon" &&
reward.rewardType === "Surprise" &&
"coupon" in reward &&
reward.coupon?.some(({ unwrapped }) => !unwrapped)
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards return { rewards }
.filter((reward) => !wrappedSurprisesIds.includes(reward.reward_id)) }),
.map((reward) => {
return {
...reward,
id: validatedApiRewards.data.find(
({ rewardId }) => rewardId === reward.reward_id
)?.id,
}
})
getCurrentRewardSuccessCounter.add(1)
return {
rewards,
nextCursor,
}
}),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1) getCurrentRewardCounter.add(1)

View File

@@ -21,7 +21,8 @@ export type ContentProps = {
} }
export interface CurrentRewardsClientProps { export interface CurrentRewardsClientProps {
initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined } rewards: Reward[]
pageSize: number
showRedeem: boolean showRedeem: boolean
} }

View File

@@ -31,19 +31,6 @@ export interface RowProps {
transaction: Transaction transaction: Transaction
} }
export interface PaginationProps {
pageCount: number
isFetching: boolean
handlePageChange: (page: number) => void
currentPage: number
}
export interface PaginationButtonProps {
disabled: boolean
isActive?: boolean
handleClick: () => void
}
export interface AwardPointsProps extends Pick<Transaction, "awardPoints"> {} export interface AwardPointsProps extends Pick<Transaction, "awardPoints"> {}
export interface AwardPointsVariantProps export interface AwardPointsVariantProps

View File

@@ -0,0 +1,12 @@
export interface PaginationProps {
pageCount: number
isFetching?: boolean
handlePageChange: (page: number) => void
currentPage: number
}
export interface PaginationButtonProps {
disabled: boolean
isActive?: boolean
handleClick: () => void
}