diff --git a/actions/editProfile.ts b/actions/editProfile.ts index 010772c4a..8f0791c48 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -4,7 +4,7 @@ import { z } from "zod" import { ApiLang } from "@/constants/languages" import * as api from "@/lib/api" -import { serverClient } from "@/lib/trpc/server" +import { getProfile } from "@/lib/trpc/memoizedRequests" import { protectedServerActionProcedure } from "@/server/trpc" import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" @@ -68,7 +68,7 @@ export const editProfile = protectedServerActionProcedure } } - const profile = await serverClient().user.get() + const profile = await getProfile() if (!profile || "error" in profile) { console.error( "editProfile profile fetch error", diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index a5b818f77..6775fd188 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -1,4 +1,7 @@ +import { Suspense } from "react" + import Breadcrumbs from "@/components/Breadcrumbs" +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -6,5 +9,9 @@ import { LangParams, PageArgs } from "@/types/params" export default function AllBreadcrumbs({ params }: PageArgs) { setLang(params.lang) - return + return ( + }> + + + ) } diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index eff4795d9..43268adca 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -1,3 +1,6 @@ +import { Suspense } from "react" + +import LoadingSpinner from "@/components/LoadingSpinner" import Sidebar from "@/components/MyPages/Sidebar" // import Surprises from "@/components/MyPages/Surprises" @@ -14,7 +17,9 @@ export default async function MyPagesLayout({
{breadcrumbs}
- + }> + + {children}
diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx index 226a33860..693bdc171 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx @@ -1,4 +1,4 @@ -import { serverClient } from "@/lib/trpc/server" +import { getMembershipCards } from "@/lib/trpc/memoizedRequests" import { PlusCircleIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" @@ -16,7 +16,7 @@ export default async function MembershipCardSlot({ }: PageArgs) { setLang(params.lang) const { formatMessage } = await getIntl() - const membershipCards = await serverClient().user.membershipCards() + const membershipCards = await getMembershipCards() return (
diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx index 4119f48e3..ec8f14553 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx @@ -1,4 +1,7 @@ +import { Suspense } from "react" + import Breadcrumbs from "@/components/Breadcrumbs" +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -6,5 +9,9 @@ import { LangParams, PageArgs } from "@/types/params" export default function PageBreadcrumbs({ params }: PageArgs) { setLang(params.lang) - return + return ( + }> + + + ) } diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index cbb83a9dc..08fa09422 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -16,7 +16,7 @@ import { export { generateMetadata } from "@/utils/generateMetadata" -export default async function ContentTypePage({ +export default function ContentTypePage({ params, }: PageArgs) { setLang(params.lang) diff --git a/app/[lang]/webview/loading.tsx b/app/[lang]/webview/loading.tsx new file mode 100644 index 000000000..c739b6635 --- /dev/null +++ b/app/[lang]/webview/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return +} diff --git a/components/Blocks/DynamicContent/OverviewTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/index.tsx index 866a0c7d6..f39fb9a25 100644 --- a/components/Blocks/DynamicContent/OverviewTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/index.tsx @@ -1,3 +1,4 @@ +import { getMembershipLevelSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import SectionWrapper from "../SectionWrapper" @@ -9,8 +10,11 @@ export default async function OverviewTable({ dynamic_content, firstItem, }: OverviewTableProps) { - const levels = await serverClient().contentstack.rewards.all() - const membershipLevel = await serverClient().user.safeMembershipLevel() + const [levels, membershipLevel] = await Promise.all([ + serverClient().contentstack.rewards.all(), + getMembershipLevelSafely(), + ]) + return ( }> + + + ) +} + +function DynamicContentBlocks(props: DynamicContentProps) { + const { dynamic_content, firstItem } = props switch (dynamic_content.component) { case DynamicContentEnum.Blocks.components.current_benefits: return diff --git a/components/Breadcrumbs/BreadcrumbsSkeleton.tsx b/components/Breadcrumbs/BreadcrumbsSkeleton.tsx new file mode 100644 index 000000000..6841c2dba --- /dev/null +++ b/components/Breadcrumbs/BreadcrumbsSkeleton.tsx @@ -0,0 +1,25 @@ +import { ChevronRightIcon, HouseIcon } from "@/components/Icons" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" + +import styles from "./breadcrumbs.module.css" + +export default function BreadcrumbsSkeleton() { + return ( + + ) +} diff --git a/components/Current/Header/HeaderFallback/index.tsx b/components/Current/Header/HeaderFallback/index.tsx index 8599beb27..7b6801a19 100644 --- a/components/Current/Header/HeaderFallback/index.tsx +++ b/components/Current/Header/HeaderFallback/index.tsx @@ -1,6 +1,6 @@ import { homeHrefs } from "@/constants/homeHrefs" import { env } from "@/env/server" -import { serverClient } from "@/lib/trpc/server" +import { getCurrentHeader } from "@/lib/trpc/memoizedRequests" import { getLang } from "@/i18n/serverContext" @@ -11,9 +11,7 @@ import TopMenu from "../TopMenu" import styles from "../header.module.css" export default async function HeaderFallback() { - const data = await serverClient().contentstack.base.currentHeader({ - lang: getLang(), - }) + const data = await getCurrentHeader(getLang()) if (!data?.header) { return null diff --git a/components/Current/Header/TopMenu/index.tsx b/components/Current/Header/TopMenu/index.tsx index dcce0b333..1472461d8 100644 --- a/components/Current/Header/TopMenu/index.tsx +++ b/components/Current/Header/TopMenu/index.tsx @@ -1,6 +1,6 @@ import { logout } from "@/constants/routes/handleAuth" import { overview } from "@/constants/routes/myPages" -import { serverClient } from "@/lib/trpc/server" +import { getName } from "@/lib/trpc/memoizedRequests" import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" @@ -23,7 +23,7 @@ export default async function TopMenu({ languageSwitcher, }: TopMenuProps) { const { formatMessage } = await getIntl() - const user = await serverClient().user.name() + const user = await getName() return (
diff --git a/components/Current/Header/index.tsx b/components/Current/Header/index.tsx index 2b376c778..26fe4b654 100644 --- a/components/Current/Header/index.tsx +++ b/components/Current/Header/index.tsx @@ -1,7 +1,11 @@ import { homeHrefs } from "@/constants/homeHrefs" import { env } from "@/env/server" -import { getLanguageSwitcher } from "@/lib/trpc/memoizedRequests" -import { serverClient } from "@/lib/trpc/server" +import { + getCurrentHeader, + getLanguageSwitcher, + getMyPagesNavigation, + getName, +} from "@/lib/trpc/memoizedRequests" import { getLang } from "@/i18n/serverContext" @@ -15,12 +19,10 @@ import styles from "./header.module.css" export default async function Header() { const [data, user, languages, navigation] = await Promise.all([ - serverClient().contentstack.base.currentHeader({ - lang: getLang(), - }), - serverClient().user.name(), + getCurrentHeader(getLang()), + getName(), getLanguageSwitcher(), - serverClient().contentstack.myPages.navigation.get(), + getMyPagesNavigation(), ]) if (!navigation || !languages || !data?.header) { diff --git a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx index eacb037c6..3c2afb9fc 100644 --- a/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx +++ b/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx @@ -1,5 +1,10 @@ import { MembershipLevelEnum } from "@/constants/membershipLevels" import { myPages } from "@/constants/routes/myPages" +import { + getMembershipLevelSafely, + getMyPagesNavigation, + getName, +} from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import Link from "@/components/TempDesignSystem/Link" @@ -16,9 +21,9 @@ export default async function MyPagesMenuWrapper() { const lang = getLang() const [intl, myPagesNavigation, user, membership] = await Promise.all([ getIntl(), - serverClient().contentstack.myPages.navigation.get(), - serverClient().user.name(), - serverClient().user.safeMembershipLevel(), + getMyPagesNavigation(), + getName(), + getMembershipLevelSafely(), ]) const membershipLevel = membership?.membershipLevel diff --git a/components/MyPages/Sidebar/index.tsx b/components/MyPages/Sidebar/index.tsx index b6169639e..5c4de4d2d 100644 --- a/components/MyPages/Sidebar/index.tsx +++ b/components/MyPages/Sidebar/index.tsx @@ -1,7 +1,7 @@ import { Fragment } from "react" import { logout } from "@/constants/routes/handleAuth" -import { serverClient } from "@/lib/trpc/server" +import { getMyPagesNavigation } from "@/lib/trpc/memoizedRequests" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" @@ -12,7 +12,7 @@ import { getLang } from "@/i18n/serverContext" import styles from "./sidebar.module.css" export default async function SidebarMyPages() { - const navigation = await serverClient().contentstack.myPages.navigation.get() + const navigation = await getMyPagesNavigation() const { formatMessage } = await getIntl() return ( diff --git a/components/Sidebar/JoinLoyalty/index.tsx b/components/Sidebar/JoinLoyalty/index.tsx index ae5f582d5..13b3a91c5 100644 --- a/components/Sidebar/JoinLoyalty/index.tsx +++ b/components/Sidebar/JoinLoyalty/index.tsx @@ -1,4 +1,4 @@ -import { serverClient } from "@/lib/trpc/server" +import { getName } from "@/lib/trpc/memoizedRequests" import LoginButton from "@/components/Current/Header/LoginButton" import ArrowRight from "@/components/Icons/ArrowRight" @@ -19,7 +19,7 @@ export default async function JoinLoyaltyContact({ block, }: JoinLoyaltyContactProps) { const intl = await getIntl() - const user = await serverClient().user.name() + const user = await getName() // Check if we have user, that means we are logged in. if (user) { diff --git a/components/Sidebar/MyPagesNavigation.tsx b/components/Sidebar/MyPagesNavigation.tsx index 23f60e144..9f1bc3cec 100644 --- a/components/Sidebar/MyPagesNavigation.tsx +++ b/components/Sidebar/MyPagesNavigation.tsx @@ -1,9 +1,9 @@ -import { serverClient } from "@/lib/trpc/server" +import { getName } from "@/lib/trpc/memoizedRequests" import MyPagesSidebar from "@/components/MyPages/Sidebar" export default async function MyPagesNavigation() { - const user = await serverClient().user.name() + const user = await getName() // Check if we have user, that means we are logged in andt the My Pages menu can show. if (!user) { diff --git a/components/TempDesignSystem/Alert/index.tsx b/components/TempDesignSystem/Alert/index.tsx index 2f1e97b6a..2c511b725 100644 --- a/components/TempDesignSystem/Alert/index.tsx +++ b/components/TempDesignSystem/Alert/index.tsx @@ -51,7 +51,7 @@ export default function Alert({ {phoneContact.displayText} {phoneContact.phoneNumber} diff --git a/components/TrackingSDK/index.tsx b/components/TrackingSDK/index.tsx index d267341b6..05fd93d9c 100644 --- a/components/TrackingSDK/index.tsx +++ b/components/TrackingSDK/index.tsx @@ -1,3 +1,4 @@ +import { getUserTracking } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import RouterTransition from "@/components/TrackingSDK/RouterTransition" @@ -7,7 +8,7 @@ import TrackingSDKClient from "./Client" import { TrackingSDKPageData } from "@/types/components/tracking" export const preloadUserTracking = () => { - void serverClient().user.tracking() + void getUserTracking() } export default async function TrackingSDK({ @@ -15,7 +16,7 @@ export default async function TrackingSDK({ }: { pageData: TrackingSDKPageData }) { - const userTrackingData = await serverClient().user.tracking() + const userTrackingData = await getUserTracking() return ( <> diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 471d20def..d09349a8c 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -1,5 +1,7 @@ import { cache } from "react" +import { Lang } from "@/constants/languages" + import { serverClient } from "../server" export const getLocations = cache(async function getMemoizedLocations() { @@ -10,6 +12,10 @@ export const getProfile = cache(async function getMemoizedProfile() { return serverClient().user.get() }) +export const getName = cache(async function getMemoizedName() { + return serverClient().user.name() +}) + export const getProfileSafely = cache( async function getMemoizedProfileSafely() { return serverClient().user.getSafely() @@ -22,6 +28,28 @@ export const getCreditCardsSafely = cache( } ) +export const getMembershipLevel = cache( + async function getMemoizedMembershipLevel() { + return serverClient().user.membershipLevel() + } +) + +export const getMembershipLevelSafely = cache( + async function getMemoizedMembershipLevelSafely() { + return serverClient().user.safeMembershipLevel() + } +) + +export const getMembershipCards = cache( + async function getMemoizedMembershipCards() { + return serverClient().user.membershipCards() + } +) + +export const getUserTracking = cache(async function getMemoizedUserTracking() { + return serverClient().user.tracking() +}) + export const getHotelData = cache(async function getMemoizedHotelData( hotelId: string, language: string @@ -40,6 +68,18 @@ export const getHeader = cache(async function getMemoizedHeader() { return serverClient().contentstack.base.header() }) +export const getCurrentHeader = cache(async function getMemoizedCurrentHeader( + lang: Lang +) { + return serverClient().contentstack.base.currentHeader({ lang }) +}) + +export const getMyPagesNavigation = cache( + async function getMemoizedMyPagesNavigation() { + return serverClient().contentstack.myPages.navigation.get() + } +) + export const getLanguageSwitcher = cache( async function getMemoizedLanguageSwitcher() { return serverClient().contentstack.languageSwitcher.get() diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 3218b8bb6..d312d6042 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -1,3 +1,5 @@ +import { cache } from "react" + import { Lang } from "@/constants/languages" import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql" import { @@ -92,7 +94,7 @@ import type { GetSiteConfigRefData, } from "@/types/trpc/routers/contentstack/siteConfig" -async function getContactConfig(lang: Lang) { +const getContactConfig = cache(async (lang: Lang) => { getContactConfigCounter.add(1, { lang }) console.info( "contentstack.contactConfig start", @@ -153,7 +155,7 @@ async function getContactConfig(lang: Lang) { JSON.stringify({ query: { lang } }) ) return validatedContactConfigConfig.data.all_contact_config.items[0] -} +}) export const baseQueryRouter = router({ contact: contentstackBaseProcedure.query(async ({ ctx }) => { diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 5b823c68d..224f03198 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,4 +1,5 @@ import { metrics } from "@opentelemetry/api" +import { cache } from "react" import * as api from "@/lib/api" import { @@ -80,86 +81,87 @@ const getCreditCardsFailCounter = meter.createCounter( "trpc.user.creditCards-fail" ) -export async function getVerifiedUser({ session }: { session: Session }) { - const now = Date.now() - - if (session.token.expires_at && session.token.expires_at < now) { - return { error: true, cause: "token_expired" } as const - } - getVerifiedUserCounter.add(1) - console.info("api.user.profile getVerifiedUser start", JSON.stringify({})) - const apiResponse = await api.get(api.endpoints.v1.profile, { - headers: { - Authorization: `Bearer ${session.token.access_token}`, - }, - }) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - getVerifiedUserFailCounter.add(1, { - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), +export const getVerifiedUser = cache( + async ({ session }: { session: Session }) => { + const now = Date.now() + if (session.token.expires_at && session.token.expires_at < now) { + return { error: true, cause: "token_expired" } as const + } + getVerifiedUserCounter.add(1) + console.info("api.user.profile getVerifiedUser start", JSON.stringify({})) + const apiResponse = await api.get(api.endpoints.v1.profile, { + headers: { + Authorization: `Bearer ${session.token.access_token}`, + }, }) - console.error( - "api.user.profile getVerifiedUser error", - JSON.stringify({ - error: { + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getVerifiedUserFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ status: apiResponse.status, statusText: apiResponse.statusText, text, - }, + }), }) - ) - if (apiResponse.status === 401) { - return { error: true, cause: "unauthorized" } as const - } else if (apiResponse.status === 403) { - return { error: true, cause: "forbidden" } as const - } else if (apiResponse.status === 404) { - return { error: true, cause: "notfound" } as const + console.error( + "api.user.profile getVerifiedUser error", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + if (apiResponse.status === 401) { + return { error: true, cause: "unauthorized" } as const + } else if (apiResponse.status === 403) { + return { error: true, cause: "forbidden" } as const + } else if (apiResponse.status === 404) { + return { error: true, cause: "notfound" } as const + } + return { + error: true, + cause: "unknown", + status: apiResponse.status, + } as const } - return { - error: true, - cause: "unknown", - status: apiResponse.status, - } as const - } - const apiJson = await apiResponse.json() - if (!apiJson.data?.attributes) { - getVerifiedUserFailCounter.add(1, { - error_type: "data_error", - }) - console.error( - "api.user.profile getVerifiedUser data error", - JSON.stringify({ - apiResponse: apiJson, + const apiJson = await apiResponse.json() + if (!apiJson.data?.attributes) { + getVerifiedUserFailCounter.add(1, { + error_type: "data_error", }) - ) - return null - } + console.error( + "api.user.profile getVerifiedUser data error", + JSON.stringify({ + apiResponse: apiJson, + }) + ) + return null + } - const verifiedData = getUserSchema.safeParse(apiJson) - if (!verifiedData.success) { - getVerifiedUserFailCounter.add(1, { - error_type: "validation_error", - error: JSON.stringify(verifiedData.error), - }) - console.error( - "api.user.profile validation error", - JSON.stringify({ - errors: verifiedData.error, + const verifiedData = getUserSchema.safeParse(apiJson) + if (!verifiedData.success) { + getVerifiedUserFailCounter.add(1, { + error_type: "validation_error", + error: JSON.stringify(verifiedData.error), }) - ) - return null + console.error( + "api.user.profile validation error", + JSON.stringify({ + errors: verifiedData.error, + }) + ) + return null + } + getVerifiedUserSuccessCounter.add(1) + console.info("api.user.profile getVerifiedUser success", JSON.stringify({})) + return verifiedData } - getVerifiedUserSuccessCounter.add(1) - console.info("api.user.profile getVerifiedUser success", JSON.stringify({})) - return verifiedData -} +) function parsedUser(data: User, isMFA: boolean) { const country = countries.find((c) => c.code === data.address.countryCode)