diff --git a/components/MyPages/AccountPage/Content.tsx b/components/MyPages/AccountPage/Content.tsx index e91dd0e74..99bd03a99 100644 --- a/components/MyPages/AccountPage/Content.tsx +++ b/components/MyPages/AccountPage/Content.tsx @@ -39,7 +39,7 @@ function DynamicComponent({ component, props }: AccountPageContentProps) { case DynamicContentComponents.expiring_points: return case DynamicContentComponents.earn_and_burn: - return + return default: return null } diff --git a/components/MyPages/Blocks/Points/CurrentPointsBalance/index.tsx b/components/MyPages/Blocks/Points/CurrentPointsBalance/index.tsx index 440cc6f11..6e93d9ba9 100644 --- a/components/MyPages/Blocks/Points/CurrentPointsBalance/index.tsx +++ b/components/MyPages/Blocks/Points/CurrentPointsBalance/index.tsx @@ -2,7 +2,7 @@ import { _ } from "@/lib/translation" import styles from "./currentPointsBalance.module.css" -const CurrentPointsBalance = () => { +function CurrentPointsBalance() { const points = 30000 return ( diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx index 694966524..21546c39c 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx +++ b/components/MyPages/Blocks/Points/EarnAndBurn/index.tsx @@ -1,44 +1,101 @@ +"use client" + +import { dt } from "@/lib/dt" import { _ } from "@/lib/translation" +import { trpc } from "@/lib/trpc/client" import Button from "@/components/TempDesignSystem/Button" import styles from "./earnAndBurn.module.css" -const EarnAndBurn = () => { +import { + EarnAndBurnProps, + Page, + RowProps, +} from "@/types/components/myPages/myPage/earnAndBurn" + +const tableHeadings = [ + _("Arrival date"), + _("Description"), + _("Booking number"), + _("Transaction date"), + _("Points"), +] + +function EarnAndBurn({ lang }: EarnAndBurnProps) { + const { data, hasNextPage, isFetching, fetchNextPage } = + trpc.user.transaction.friendTransactions.useInfiniteQuery( + { limit: 5 }, + { + getNextPageParam: (lastPage: Page) => lastPage.nextCursor, + } + ) + + function loadMoreData() { + if (hasNextPage) { + fetchNextPage() + } + } + + const transactions = data?.pages.flatMap((page) => page.data) ?? [] + return (
- - - - - + {tableHeadings.map((heading) => ( + + ))} - - - - - - - - - - - - - - + {transactions.map((transaction) => ( + + ))}
{_("Arrival date")}{_("Description")}{_("Booking number")}{_("Transaction date")}{_("Points")} + {heading} +
23 May 2023Scandic Mölndal, Mölndal 2 nights512345657626 May 202330000
23 May 2023Scandic Mölndal, Mölndal 2 nights512345657626 May 2023-30000
-
) } +function Row({ transaction, lang }: RowProps) { + const description = `${_(transaction.hotelName)}, ${transaction.city} ${transaction.nights} ${_("nights")}` + const arrival = dt(transaction.checkInDate).locale(lang).format("DD MMM YYYY") + const departure = dt(transaction.checkOutDate) + .locale(lang) + .format("DD MMM YYYY") + const values = [ + arrival, + description, + transaction.confirmationNumber, + departure, + transaction.awardPoints, + ] + return ( + + {values.map((value, idx) => ( + + {value} + + ))} + + ) +} + export default EarnAndBurn diff --git a/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx b/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx index 07815048c..98cf04ab0 100644 --- a/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx +++ b/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx @@ -2,7 +2,7 @@ import { _ } from "@/lib/translation" import styles from "./expiringPoints.module.css" -export const ExpiringPoints = () => { +export function ExpiringPoints() { return ( diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 2efc39cd8..2dd4f268c 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -9,6 +9,7 @@ export namespace endpoints { profile = "profile/v1/Profile", upcomingStays = "booking/v1/Stays/future", previousStays = "booking/v1/Stays/past", + friendTransactions = "profile/v1/Transaction/friendTransactions", } } diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index a130e6ae2..2baf98618 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -12,3 +12,8 @@ export const soonestUpcomingStaysInput = z limit: z.number().int().positive(), }) .default({ limit: 3 }) + +export const friendTransactionsInput = z.object({ + cursor: z.number().int().nullish(), + limit: z.number().min(0).default(5), +}) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index 1ad24f2ae..0432b7ed1 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -93,3 +93,72 @@ export const getStaysSchema = z.object({ type GetStaysData = z.infer export type Stay = GetStaysData["data"][number] + +export const getFriendTransactionsSchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + hotelOperaId: z.string(), + confirmationNumber: z.string(), + checkInDate: z.string(), + checkOutDate: z.string(), + nights: z.number(), + awardPoints: z.number(), + pointsCalculated: z.boolean(), + hotelInformation: z.object({ + hotelName: z.string(), + city: z.string(), + hotelContent: z.object({ + images: z.object({ + metaData: z.object({ + title: z.string(), + altText: z.string(), + altText_En: z.string(), + copyRight: z.string(), + }), + imageSizes: z.object({ + tiny: z.string(), + small: z.string(), + medium: z.string(), + large: z.string(), + }), + }), + }), + }), + }), + relationships: z.object({ + hotel: z.object({ + links: z.object({ + related: z.string(), + }), + data: z.object({ + id: z.string(), + type: z.string(), + }), + }), + booking: z.object({ + links: z.object({ + related: z.string(), + }), + data: z.object({ + id: z.string(), + type: z.string(), + }), + }), + }), + type: z.string(), + }) + ), + links: z + .object({ + self: z.string(), + offset: z.number(), + limit: z.number(), + totalCount: z.number(), + }) + .nullable(), +}) + +type GetFriendTransactionsData = z.infer + +export type Transaction = GetFriendTransactionsData["data"][number] diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index e22ad465d..e0c2de017 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -8,9 +8,14 @@ import { } from "@/server/errors/trpc" import { protectedProcedure, router } from "@/server/trpc" -import { staysInput } from "./input" -import { getStaysSchema, getUserSchema } from "./output" +import { friendTransactionsInput, staysInput } from "./input" +import { + getFriendTransactionsSchema, + getStaysSchema, + getUserSchema, +} from "./output" import { benefits, extendedUser, nextLevelPerks } from "./temp" +import friendTransactionsMockJson from "./tempFriendTransactions.json" function fakingRequest(payload: T): Promise { return new Promise((resolve) => { @@ -177,4 +182,83 @@ export const userQueryRouter = router({ } }), }), + transaction: router({ + friendTransactions: protectedProcedure + .input(friendTransactionsInput) + .query(async (opts) => { + try { + const { limit, cursor } = opts.input + + const params = new URLSearchParams() + params.set("limit", limit.toString()) + + if (cursor) { + params.set("offset", cursor.toString()) + } + // TODO: att these once API data is confirmed to be available + // const apiResponse = await api.get( + // api.endpoints.v1.friendTransactions, + // { + // headers: { + // Authorization: `Bearer ${opts.ctx.session.token.access_token}`, + // }, + // }, + // params + // ) + + // if (!apiResponse.ok) { + // switch (apiResponse.status) { + // case 400: + // throw badRequestError() + // case 401: + // throw unauthorizedError() + // case 403: + // throw forbiddenError() + // default: + // throw internalServerError() + // } + // } + + // const apiJson = await apiResponse.json() + const apiJson = friendTransactionsMockJson + debugger + + if (!apiJson.data?.length) { + // throw internalServerError() + } + + const verifiedData = getFriendTransactionsSchema.safeParse(apiJson) + + if (!verifiedData.success) { + console.info(`Get Friend Transactions - Verified Data Error`) + console.error(verifiedData.error) + throw badRequestError() + } + + const nextCursor = + verifiedData.data.links && + verifiedData.data.links.offset < verifiedData.data.links.totalCount + ? verifiedData.data.links.offset + : undefined + + return { + data: verifiedData.data.data.map(({ attributes }) => ({ + checkInDate: attributes.checkInDate, + checkOutDate: attributes.checkOutDate, + awardPoints: attributes.awardPoints, + hotelName: attributes.hotelInformation.hotelName, + city: attributes.hotelInformation.city, + nights: attributes.nights, + confirmationNumber: attributes.confirmationNumber, + })), + + nextCursor, + } + } catch (error) { + console.info(`Get Friend Transactions Error`) + console.error(error) + throw internalServerError() + } + }), + }), }) diff --git a/server/routers/user/tempFriendTransactions.json b/server/routers/user/tempFriendTransactions.json new file mode 100644 index 000000000..b3340d021 --- /dev/null +++ b/server/routers/user/tempFriendTransactions.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "attributes": { + "hotelOperaId": "216", + "confirmationNumber": "991646189", + "checkInDate": "2023-09-16", + "checkOutDate": "2023-09-17", + "transactionDate": "2023-04-18", + "nights": 1, + "awardPoints": 1863, + "pointsCalculated": true, + "hotelInformation": { + "hotelName": "Scandic Landvetter", + "city": "Stockholm", + "hotelContent": { + "images": { + "metaData": { + "title": "Lobby", + "altText": "lobby at scandic landvetter in gothenburg", + "altText_En": "lobby at scandic landvetter in gothenburg", + "copyRight": "Werner Nystrand" + }, + "imageSizes": { + "tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/1cz71gn106ej1mz7u4nr/Scandic-Landvetter-lobby-0013-2-vald.jpg", + "small": "https://test3.scandichotels.com/imagevault/publishedmedia/29ejr75mwp7riv63nz0x/Scandic-Landvetter-lobby-0013-2-vald.jpg", + "medium": "https://test3.scandichotels.com/imagevault/publishedmedia/bldh2liyfddkv74szp9v/Scandic-Landvetter-lobby-0013-2-vald.jpg", + "large": "https://test3.scandichotels.com/imagevault/publishedmedia/kbmpmkb714o028ufcgu4/Scandic-Landvetter-lobby-0013-2-vald.jpg" + } + } + } + } + }, + "relationships": { + "hotel": { + "links": { + "related": "https://api-test.scandichotels.com/hotels/V0/fdea883a-8092-4604-8afb-032391a59009/hotels" + }, + "data": { + "id": "d98c7ab1-ebaa-4102-b351-758daf1ddf55", + "type": "hotels" + } + }, + "booking": { + "links": { + "related": "https://api-test.scandichotels.com/booking/v1/bookings/991646189" + }, + "data": { + "id": "991646189", + "type": "booking" + } + } + }, + "type": "stay" + }, + { + "attributes": { + "hotelOperaId": "216", + "confirmationNumber": "991646190", + "checkInDate": "2023-09-16", + "checkOutDate": "2023-09-17", + "transactionDate": "2023-04-18", + "nights": 1, + "awardPoints": 1863, + "pointsCalculated": true, + "hotelInformation": { + "hotelName": "Scandic Landvetter", + "city": "Stockholm", + "hotelContent": { + "images": { + "metaData": { + "title": "Lobby", + "altText": "lobby at scandic landvetter in gothenburg", + "altText_En": "lobby at scandic landvetter in gothenburg", + "copyRight": "Werner Nystrand" + }, + "imageSizes": { + "tiny": "https://test3.scandichotels.com/imagevault/publishedmedia/1cz71gn106ej1mz7u4nr/Scandic-Landvetter-lobby-0013-2-vald.jpg", + "small": "https://test3.scandichotels.com/imagevault/publishedmedia/29ejr75mwp7riv63nz0x/Scandic-Landvetter-lobby-0013-2-vald.jpg", + "medium": "https://test3.scandichotels.com/imagevault/publishedmedia/bldh2liyfddkv74szp9v/Scandic-Landvetter-lobby-0013-2-vald.jpg", + "large": "https://test3.scandichotels.com/imagevault/publishedmedia/kbmpmkb714o028ufcgu4/Scandic-Landvetter-lobby-0013-2-vald.jpg" + } + } + } + } + }, + "relationships": { + "hotel": { + "links": { + "related": "https://api-test.scandichotels.com/hotels/V0/fdea883a-8092-4604-8afb-032391a59009/hotels" + }, + "data": { + "id": "d98c7ab1-ebaa-4102-b351-758daf1ddf55", + "type": "hotels" + } + }, + "booking": { + "links": { + "related": "https://api-test.scandichotels.com/booking/v1/bookings/991646189" + }, + "data": { + "id": "991646189", + "type": "booking" + } + } + }, + "type": "stay" + } + ], + "links": { + "self": "https://api-test.scandichotels.com/profile/v1/transaction/friendTransactions?language=en&offset=1&limit=20", + "offset": 2, + "limit": 20, + "totalCount": 40 + } +} diff --git a/types/components/myPages/myPage/earnAndBurn.ts b/types/components/myPages/myPage/earnAndBurn.ts new file mode 100644 index 000000000..7c0547b78 --- /dev/null +++ b/types/components/myPages/myPage/earnAndBurn.ts @@ -0,0 +1,33 @@ +import { Lang } from "@/constants/languages" + +export type EarnAndBurnProps = { + lang: Lang +} + +type Transaction = { + checkInDate: string + checkOutDate: string + awardPoints: number + hotelName: string + city: string + nights: number + confirmationNumber: string +} + +export type Page = { + data: Transaction[] + nextCursor?: number +} + +export type RowProps = { + lang: Lang + transaction: { + checkInDate: string + checkOutDate: string + awardPoints: number + hotelName: string + city: string + nights: number + confirmationNumber: string + } +}