From 661a8019d39b2827d095d4745bc12a5e9ab9621c Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 8 May 2024 14:56:39 +0200 Subject: [PATCH] feat: enhance stays with api data --- .../MyPages/Blocks/Stays/Header/index.tsx | 2 +- .../MyPages/Blocks/Stays/Previous/index.tsx | 2 +- .../Blocks/Stays/ShowMoreButton/index.tsx | 2 +- .../MyPages/Blocks/Stays/Soonest/index.tsx | 2 +- .../MyPages/Blocks/Stays/StayCard/index.tsx | 21 +-- .../MyPages/Blocks/Stays/StayList/index.tsx | 8 +- .../MyPages/Blocks/Stays/Upcoming/index.tsx | 9 +- lib/api/endpoints.ts | 6 +- lib/api/index.ts | 8 +- server/routers/user/input.ts | 14 +- server/routers/user/output.ts | 66 +++++++ server/routers/user/query.ts | 176 ++++++++++++------ types/components/myPages/myPage/stay.ts | 9 - .../myPages/{myStays => stays}/button.ts | 0 .../myPages/{myStays => stays}/page.ts | 2 +- .../myPages/{myStays => stays}/stayCard.ts | 2 +- .../myPages/{myStays => stays}/stayList.ts | 2 +- .../myPages/{myStays => stays}/title.ts | 0 18 files changed, 223 insertions(+), 108 deletions(-) delete mode 100644 types/components/myPages/myPage/stay.ts rename types/components/myPages/{myStays => stays}/button.ts (100%) rename types/components/myPages/{myStays => stays}/page.ts (53%) rename types/components/myPages/{myStays => stays}/stayCard.ts (65%) rename types/components/myPages/{myStays => stays}/stayList.ts (66%) rename types/components/myPages/{myStays => stays}/title.ts (100%) diff --git a/components/MyPages/Blocks/Stays/Header/index.tsx b/components/MyPages/Blocks/Stays/Header/index.tsx index 47fb0ebd8..746c31850 100644 --- a/components/MyPages/Blocks/Stays/Header/index.tsx +++ b/components/MyPages/Blocks/Stays/Header/index.tsx @@ -4,7 +4,7 @@ import Title from "@/components/Title" import styles from "./header.module.css" -import type { HeaderProps } from "@/types/components/myPages/myStays/title" +import type { HeaderProps } from "@/types/components/myPages/stays/title" export default function Header({ title, subtitle, link }: HeaderProps) { return ( diff --git a/components/MyPages/Blocks/Stays/Previous/index.tsx b/components/MyPages/Blocks/Stays/Previous/index.tsx index f5205da1d..f4416fba2 100644 --- a/components/MyPages/Blocks/Stays/Previous/index.tsx +++ b/components/MyPages/Blocks/Stays/Previous/index.tsx @@ -10,7 +10,7 @@ import StayList from "../StayList" import EmptyPreviousStaysBlock from "./EmptyPreviousStays" import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" -import type { Page } from "@/types/components/myPages/myStays/page" +import type { Page } from "@/types/components/myPages/stays/page" export default function PreviousStays({ lang, diff --git a/components/MyPages/Blocks/Stays/ShowMoreButton/index.tsx b/components/MyPages/Blocks/Stays/ShowMoreButton/index.tsx index cd9a3a8d9..abf15c216 100644 --- a/components/MyPages/Blocks/Stays/ShowMoreButton/index.tsx +++ b/components/MyPages/Blocks/Stays/ShowMoreButton/index.tsx @@ -2,7 +2,7 @@ import Button from "@/components/TempDesignSystem/Button" import styles from "./button.module.css" -import type { ShowMoreButtonParams } from "@/types/components/myPages/myStays/button" +import type { ShowMoreButtonParams } from "@/types/components/myPages/stays/button" export default function ShowMoreButton({ disabled, diff --git a/components/MyPages/Blocks/Stays/Soonest/index.tsx b/components/MyPages/Blocks/Stays/Soonest/index.tsx index 055d776ec..28eea7801 100644 --- a/components/MyPages/Blocks/Stays/Soonest/index.tsx +++ b/components/MyPages/Blocks/Stays/Soonest/index.tsx @@ -16,7 +16,7 @@ export default async function SoonestStays({ subtitle, link, }: AccountPageComponentProps) { - const stays = await serverClient().user.stays.soonestUpcoming() + const { data: stays } = await serverClient().user.stays.upcoming({ limit: 3 }) return ( diff --git a/components/MyPages/Blocks/Stays/StayCard/index.tsx b/components/MyPages/Blocks/Stays/StayCard/index.tsx index c7e36641a..8bd0e0a86 100644 --- a/components/MyPages/Blocks/Stays/StayCard/index.tsx +++ b/components/MyPages/Blocks/Stays/StayCard/index.tsx @@ -5,15 +5,15 @@ import Title from "@/components/Title" import styles from "./stay.module.css" -import type { StayCardProps } from "@/types/components/myPages/myStays/stayCard" +import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" export default function StayCard({ stay, lang }: StayCardProps) { - const { dateArrive, dateDepart, guests, hotel } = stay + const { checkinDate, checkoutDate, hotelInformation } = stay.attributes - const arrival = dt(dateArrive).locale(lang) + const arrival = dt(checkinDate).locale(lang) const arrivalDate = arrival.format("DD MMM") const arrivalDateTime = arrival.format("YYYY-MM-DD") - const depart = dt(dateDepart).locale(lang) + const depart = dt(checkoutDate).locale(lang) const departDate = depart.format("DD MMM YYYY") const departDateTime = depart.format("YYYY-MM-DD") @@ -27,7 +27,7 @@ export default function StayCard({ stay, lang }: StayCardProps) { uppercase className={styles.hotel} > - {hotel} + {hotelInformation.hotelName}
@@ -43,17 +43,6 @@ export default function StayCard({ stay, lang }: StayCardProps) {

-
- Guests Icon - - {guests} guest{guests > 1 ? "s" : ""} - -
diff --git a/components/MyPages/Blocks/Stays/StayList/index.tsx b/components/MyPages/Blocks/Stays/StayList/index.tsx index 08efae29b..c3ad71cd7 100644 --- a/components/MyPages/Blocks/Stays/StayList/index.tsx +++ b/components/MyPages/Blocks/Stays/StayList/index.tsx @@ -2,13 +2,17 @@ import StayCard from "../StayCard" import styles from "./stayList.module.css" -import { StayListProps } from "@/types/components/myPages/myStays/stayList" +import { StayListProps } from "@/types/components/myPages/stays/stayList" export default function StayList({ lang, stays }: StayListProps) { return (
{stays.map((stay) => ( - + ))}
) diff --git a/components/MyPages/Blocks/Stays/Upcoming/index.tsx b/components/MyPages/Blocks/Stays/Upcoming/index.tsx index 05d4f3882..c33dedcbe 100644 --- a/components/MyPages/Blocks/Stays/Upcoming/index.tsx +++ b/components/MyPages/Blocks/Stays/Upcoming/index.tsx @@ -1,6 +1,5 @@ "use client" -import { Lang } from "@/constants/languages" import { _ } from "@/lib/translation" import { trpc } from "@/lib/trpc/client" @@ -12,7 +11,7 @@ import StayList from "../StayList" import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays" import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" -import type { Page } from "@/types/components/myPages/myStays/page" +import type { Page } from "@/types/components/myPages/stays/page" export default function UpcomingStays({ lang, @@ -22,14 +21,16 @@ export default function UpcomingStays({ }: AccountPageComponentProps) { const { data, hasNextPage, isFetching, fetchNextPage } = trpc.user.stays.upcoming.useInfiniteQuery( - {}, + { limit: 6 }, { getNextPageParam: (lastPage: Page) => lastPage.nextCursor, } ) function loadMoreData() { - fetchNextPage() + if (hasNextPage) { + fetchNextPage() + } } return ( diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 85ae58336..d44a78891 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -5,6 +5,10 @@ export namespace endpoints { export const enum v0 { profile = "profile/v0/Profile", } + export const enum v1 { + upcomingStays = "booking/v1/Stays/future", + previousStays = "booking/v1/Stays/past", + } } -export type Endpoint = endpoints.v0 +export type Endpoint = endpoints.v0 | endpoints.v1 diff --git a/lib/api/index.ts b/lib/api/index.ts index 108381e11..900889549 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -20,10 +20,14 @@ const defaultOptions: RequestInit = { export async function get( endpoint: Endpoint, - options: RequestOptionsWithOutBody + options: RequestOptionsWithOutBody, + params?: URLSearchParams ) { defaultOptions.method = "GET" - return fetch(`${env.API_BASEURL}/${endpoint}`, merge(defaultOptions, options)) + const url = new URL( + `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}` + ) + return fetch(url, merge(defaultOptions, options)) } export async function patch( diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index f48d56b06..a130e6ae2 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -2,15 +2,13 @@ import { z } from "zod" export const staysInput = z .object({ - perPage: z.number().min(0).default(6), - page: z.number().min(0).default(0), + limit: z.number().min(0).default(6), cursor: z.number().nullish(), }) .default({}) - - export const soonestUpcomingStaysInput = z - .object({ - limit: z.number().int().positive(), - }) - .default({ limit: 3 }) \ No newline at end of file +export const soonestUpcomingStaysInput = z + .object({ + limit: z.number().int().positive(), + }) + .default({ limit: 3 }) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index a38e735af..16452f742 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -23,3 +23,69 @@ export const getUserSchema = z.object({ phoneNumber: z.string(), profileId: z.string(), }) + +// Schema is the same for upcoming and previous stays endpoints +export const getStaysSchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + hotelOperaId: z.string(), + hotelInformation: z.object({ + 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(), + }), + }), + }), + hotelName: z.string(), + cityName: z.string(), + }), + confirmationNumber: z.string(), + checkinDate: z.string(), + checkoutDate: z.string(), + isWebAppOrigin: z.boolean(), + }), + relationships: z.object({ + hotel: z.object({ + links: z.object({ + related: z.string(), + }), + data: z.object({ + id: z.string(), + type: z.string(), + }), + }), + }), + type: z.string(), + id: z.string(), + links: z.object({ + self: z.object({ + href: z.string(), + meta: z.object({ + method: z.string(), + }), + }), + }), + }) + ), + links: z.object({ + self: z.string(), + offset: z.number(), + limit: z.number(), + totalCount: z.number(), + }), +}) + +type GetStaysData = z.infer + +export type Stay = GetStaysData["data"][number] diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 091389fe7..25032645c 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -9,15 +9,9 @@ import { } from "@/server/errors/trpc" import { protectedProcedure, router } from "@/server/trpc" -import { soonestUpcomingStaysInput, staysInput } from "./input" -import { getUserSchema } from "./output" -import { - benefits, - extendedUser, - nextLevelPerks, - previousStays, - upcomingStays, -} from "./temp" +import { staysInput } from "./input" +import { getStaysSchema, getUserSchema } from "./output" +import { benefits, extendedUser, nextLevelPerks } from "./temp" function fakingRequest(payload: T): Promise { return new Promise((resolve) => { @@ -84,66 +78,130 @@ export const userQueryRouter = router({ }), stays: router({ - soonestUpcoming: protectedProcedure - .input(soonestUpcomingStaysInput) - .query(async ({ input }) => { - return upcomingStays.slice(0, input.limit) - }), - previous: protectedProcedure.input(staysInput).query(async (opts) => { - const { perPage, page, cursor } = opts.input - let nextCursor: typeof cursor | undefined = undefined - const nrPages = Math.ceil(previousStays.length / perPage) + try { + const { limit: perPage, cursor } = opts.input - let stays, nextPage - if (cursor) { - stays = previousStays.slice(cursor, perPage + cursor + 1) - nextPage = cursor / perPage + 1 - } else { - stays = previousStays.slice( - page * perPage, - page * perPage + perPage + 1 - ) - } + const params = new URLSearchParams() + params.set("limit", perPage.toString()) - if ( - (nextPage && nextPage < nrPages && stays.length == perPage + 1) || - (!nextPage && nrPages > 1) - ) { - const nextItem = stays.pop() - if (nextItem) { - nextCursor = previousStays.indexOf(nextItem) + if (cursor) { + params.set("offset", cursor.toString()) } - } // TODO: Make request to get user data from Scandic API - return { data: stays, nextCursor } + + const apiResponse = await api.get( + api.endpoints.v1.previousStays, + { + 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() + if (!apiJson.data?.length) { + throw internalServerError() + } + + const verifiedData = getStaysSchema.safeParse(apiJson) + + if (!verifiedData.success) { + console.info(`Get Previous Stays - Verified Data Error`) + console.error(verifiedData.error) + throw badRequestError() + } + + const nextCursor = + verifiedData.data.links.offset < verifiedData.data.links.totalCount + ? verifiedData.data.links.offset + : undefined + + return { + data: verifiedData.data.data, + nextCursor, + } + } catch (error) { + console.info(`Get Previous Stays Error`) + console.error(error) + throw internalServerError() + } }), upcoming: protectedProcedure.input(staysInput).query(async (opts) => { - const { perPage, page, cursor } = opts.input - let nextCursor: typeof cursor | undefined = undefined - const nrPages = Math.ceil(upcomingStays.length / perPage) + try { + const { limit: perPage, cursor } = opts.input - let stays, nextPage - if (cursor) { - stays = upcomingStays.slice(cursor, perPage + cursor + 1) - nextPage = cursor / perPage + 1 - } else { - stays = upcomingStays.slice( - page * perPage, - page * perPage + perPage + 1 - ) - } + const params = new URLSearchParams() + params.set("limit", perPage.toString()) - if ( - (nextPage && nextPage < nrPages && stays.length == perPage + 1) || - (!nextPage && nrPages > 1) - ) { - const nextItem = stays.pop() - if (nextItem) { - nextCursor = upcomingStays.indexOf(nextItem) + if (cursor) { + params.set("offset", cursor.toString()) } - } // TODO: Make request to get user data from Scandic API - return { data: stays, nextCursor } + + const apiResponse = await api.get( + api.endpoints.v1.upcomingStays, + { + 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() + if (!apiJson.data?.length) { + throw internalServerError() + } + + const verifiedData = getStaysSchema.safeParse(apiJson) + + if (!verifiedData.success) { + console.info(`Get Upcoming Stays - Verified Data Error`) + console.error(verifiedData.error) + throw badRequestError() + } + + const nextCursor = + verifiedData.data.links.offset < verifiedData.data.links.totalCount + ? verifiedData.data.links.offset + : undefined + + return { + data: verifiedData.data.data, + nextCursor, + } + } catch (error) { + console.info(`Get Upcoming Stays Error`) + console.error(error) + throw internalServerError() + } }), }), }) diff --git a/types/components/myPages/myPage/stay.ts b/types/components/myPages/myPage/stay.ts deleted file mode 100644 index ec73f6ae1..000000000 --- a/types/components/myPages/myPage/stay.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UUID } from "crypto" - -export type Stay = { - uid: UUID - dateArrive: Date - dateDepart: Date - guests: number - hotel: string -} diff --git a/types/components/myPages/myStays/button.ts b/types/components/myPages/stays/button.ts similarity index 100% rename from types/components/myPages/myStays/button.ts rename to types/components/myPages/stays/button.ts diff --git a/types/components/myPages/myStays/page.ts b/types/components/myPages/stays/page.ts similarity index 53% rename from types/components/myPages/myStays/page.ts rename to types/components/myPages/stays/page.ts index c7f05a80f..7a76b91d1 100644 --- a/types/components/myPages/myStays/page.ts +++ b/types/components/myPages/stays/page.ts @@ -1,4 +1,4 @@ -import type { Stay } from "../myPage/stay" +import { Stay } from "@/server/routers/user/output" export type Page = { data: Stay[] diff --git a/types/components/myPages/myStays/stayCard.ts b/types/components/myPages/stays/stayCard.ts similarity index 65% rename from types/components/myPages/myStays/stayCard.ts rename to types/components/myPages/stays/stayCard.ts index ba884ab89..380c0b82b 100644 --- a/types/components/myPages/myStays/stayCard.ts +++ b/types/components/myPages/stays/stayCard.ts @@ -1,5 +1,5 @@ import type { Lang } from "@/constants/languages" -import type { Stay } from "../myPage/stay" +import type { Stay } from "@/server/routers/user/output" export type StayCardProps = { lang: Lang diff --git a/types/components/myPages/myStays/stayList.ts b/types/components/myPages/stays/stayList.ts similarity index 66% rename from types/components/myPages/myStays/stayList.ts rename to types/components/myPages/stays/stayList.ts index d76b035d8..b5dc45a83 100644 --- a/types/components/myPages/myStays/stayList.ts +++ b/types/components/myPages/stays/stayList.ts @@ -1,5 +1,5 @@ import type { Lang } from "@/constants/languages" -import type { Stay } from "../myPage/stay" +import type { Stay } from "@/server/routers/user/output" export type StayListProps = { stays: Stay[] diff --git a/types/components/myPages/myStays/title.ts b/types/components/myPages/stays/title.ts similarity index 100% rename from types/components/myPages/myStays/title.ts rename to types/components/myPages/stays/title.ts