Merged in feat/memberships-endpoint (pull request #247)

Feat(WEB-296): Add membership cards endpoint
This commit is contained in:
Matilda Landström
2024-06-19 15:37:29 +00:00
committed by Michael Zetterberg
11 changed files with 182 additions and 8 deletions

View File

@@ -8,3 +8,15 @@
display: grid;
gap: var(--Spacing-x1);
}
.card {
margin-top: 2rem;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, auto);
gap: 0.5rem;
}
.subTitle {
grid-column: span 2;
}

View File

@@ -1,3 +1,5 @@
import { serverClient } from "@/lib/trpc/server"
import { PlusCircleIcon } from "@/components/Icons"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
@@ -8,6 +10,8 @@ import styles from "./page.module.css"
export default async function CreditCardSlot() {
const { formatMessage } = await getIntl()
const creditCards = await serverClient().user.creditCards()
return (
<section className={styles.container}>
<article className={styles.content}>
@@ -20,6 +24,21 @@ export default async function CreditCardSlot() {
})}
</Body>
</article>
{creditCards &&
creditCards.length > 0 &&
creditCards.map((card, idx) => (
<div className={styles.card} key={idx}>
<Subtitle className={styles.subTitle}>
Name: {card.attribute.cardName}
</Subtitle>
<span> Type: {card.attribute.cardType} </span>
<span> Alias: {card.attribute.alias}</span>
<span> Number: {card.attribute.truncatedNumber}</span>
<span>
Expiration Date: {card.attribute.expirationDate.split("T")[0]}
</span>
</div>
))}
<Link href="#" variant="icon">
<PlusCircleIcon color="burgundy" />
<Body color="burgundy" textTransform="underlined">

View File

@@ -0,0 +1 @@
export { default } from "../page"

View File

@@ -0,0 +1,22 @@
.container {
display: grid;
gap: var(--Spacing-x3);
max-width: 510px;
}
.content {
display: grid;
gap: var(--Spacing-x1);
}
.card {
margin-top: 2rem;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, auto);
gap: 0.5rem;
}
.subTitle {
grid-column: span 2;
}

View File

@@ -0,0 +1,43 @@
import { serverClient } from "@/lib/trpc/server"
import { PlusCircleIcon } from "@/components/Icons"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import styles from "./page.module.css"
export default async function MembershipCardSlot() {
const { formatMessage } = await getIntl()
const membershipCards = await serverClient().user.membershipCards()
return (
<section className={styles.container}>
<article className={styles.content}>
<Subtitle color="black">
{formatMessage({ id: "My membership cards" })}
</Subtitle>
</article>
{membershipCards &&
membershipCards.length > 0 &&
membershipCards.map((card, idx) => (
<div className={styles.card} key={idx}>
<Subtitle className={styles.subTitle}>
Name: {card.membershipType}
</Subtitle>
<span> Current Points: {card.currentPoints} </span>
<span> Member Since: {card.memberSince}</span>
<span> Number: {card.membershipNumber}</span>
<span>Expiration Date: {card.expirationDate.split("T")[0]}</span>
</div>
))}
<Link href="#" variant="icon">
<PlusCircleIcon color="burgundy" />
<Body color="burgundy" textTransform="underlined">
{formatMessage({ id: "Add new card" })}
</Body>
</Link>
</section>
)
}

View File

@@ -6,6 +6,7 @@ export default function ProfileLayout({
children,
communication,
creditCards,
membershipCards,
profile,
}: React.PropsWithChildren<ProfileLayoutProps>) {
return (
@@ -14,6 +15,7 @@ export default function ProfileLayout({
<section className="profile-layout">
{profile}
{creditCards}
{membershipCards}
<Divider color="burgundy" opacity={8} />
{communication}
</section>

14
package-lock.json generated
View File

@@ -6890,12 +6890,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"devOptional": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -9453,9 +9453,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"devOptional": true,
"dependencies": {
"to-regex-range": "^5.0.1"

View File

@@ -181,3 +181,13 @@ export const getCreditCardsSchema = z.object({
})
),
})
export const getMembershipCardsSchema = z.array(
z.object({
currentPoints: z.number(),
expirationDate: z.string(),
membershipNumber: z.string(),
memberSince: z.string(),
membershipType: z.string(),
})
)

View File

@@ -5,6 +5,7 @@ import { protectedProcedure, router } from "@/server/trpc"
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
import * as maskValue from "@/utils/maskValue"
import { getMembershipCards } from "@/utils/user"
import {
friendTransactionsInput,
@@ -14,6 +15,7 @@ import {
import {
getCreditCardsSchema,
getFriendTransactionsSchema,
getMembershipCardsSchema,
getStaysSchema,
getUserSchema,
} from "./output"
@@ -348,4 +350,45 @@ export const userQueryRouter = router({
return verifiedData.data.data
}),
membershipCards: protectedProcedure.query(async function ({ ctx }) {
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError()
// case 401:
// throw unauthorizedError()
// case 403:
// throw forbiddenError()
// default:
// throw internalServerError()
// }
console.info(`API Response Failed - Getting Membership Cards`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
}
const apiJson = await apiResponse.json()
const verifiedData = getMembershipCardsSchema.safeParse(
apiJson.data.attributes.memberships
)
if (!verifiedData.success) {
console.info(`Failed to validate Memberships Cards Data`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
const cards = getMembershipCards(verifiedData.data)
return cards
}),
})

View File

@@ -1,5 +1,6 @@
export type ProfileLayoutProps = {
communication: React.ReactNode
creditCards: React.ReactNode
membershipCards: React.ReactNode
profile: React.ReactNode
}

View File

@@ -1,12 +1,33 @@
import { z } from "zod"
import { getMembershipCardsSchema } from "@/server/routers/user/output"
import { User } from "@/types/user"
enum scandicMemberships {
guestpr = "guestpr",
scandicfriends = "scandicfriend's",
}
export function getMembership(memberships: User["memberships"]) {
return memberships?.find(
(membership) =>
membership.membershipType.toLowerCase() === "guestpr" || "scandicfriend's"
membership.membershipType.toLowerCase() === scandicMemberships.guestpr
)
}
export function getMembershipCards(
memberships: z.infer<typeof getMembershipCardsSchema>
) {
return memberships.filter(function (membership) {
return (
membership.membershipType.toLowerCase() !== scandicMemberships.guestpr &&
membership.membershipType.toLowerCase() !==
scandicMemberships.scandicfriends
)
})
}
export function getInitials(
firstName: User["firstName"],
lastName: User["lastName"]