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 LoadingSpinner from "@/components/LoadingSpinner"
import Pagination from "@/components/MyPages/Pagination"
import ClientTable from "./ClientTable"
import Pagination from "./Pagination"
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
export const rewardsByLevelInput = z.object({
@@ -12,12 +11,6 @@ export const rewardsAllInput = z
.object({ unique: z.boolean() })
.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(
z.object({
rewardId: z.string(),

View File

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

View File

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

View File

@@ -31,19 +31,6 @@ export interface RowProps {
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 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
}