diff --git a/app/[lang]/(live)/(protected)/(.)logout/page.tsx b/app/[lang]/(live)/(protected)/(.)logout/page.tsx
new file mode 100644
index 000000000..c4884586f
--- /dev/null
+++ b/app/[lang]/(live)/(protected)/(.)logout/page.tsx
@@ -0,0 +1,14 @@
+"use client"
+
+import { useEffect } from "react"
+
+import LoadingSpinner from "@/components/LoadingSpinner"
+
+export default function LogoutInterceptedRoute() {
+ // Reload the browser on logout in order to flush router cache. This is to make sure we don't show stale user specific data.
+ useEffect(() => {
+ window.location.reload()
+ }, [])
+
+ return
+}
diff --git a/app/[lang]/(live)/(protected)/layout.tsx b/app/[lang]/(live)/(protected)/layout.tsx
index 9273fda74..c6e4a0a51 100644
--- a/app/[lang]/(live)/(protected)/layout.tsx
+++ b/app/[lang]/(live)/(protected)/layout.tsx
@@ -46,10 +46,13 @@ export default async function ProtectedLayout({
redirect(redirectURL)
case "notfound":
console.error(`[layout:protected] notfound user loading error`)
+ break
case "unknown":
console.error(`[layout:protected] unknown user loading error`)
+ break
default:
console.error(`[layout:protected] unhandled user loading error`)
+ break
}
return
Something went wrong!
}
diff --git a/app/api/web/revalidate/loyaltyConfig/route.ts b/app/api/web/revalidate/loyaltyConfig/route.ts
new file mode 100644
index 000000000..5e8fd51c7
--- /dev/null
+++ b/app/api/web/revalidate/loyaltyConfig/route.ts
@@ -0,0 +1,90 @@
+import { revalidateTag } from "next/cache"
+import { headers } from "next/headers"
+import { NextRequest } from "next/server"
+import { z } from "zod"
+
+import { Lang } from "@/constants/languages"
+import { env } from "@/env/server"
+import { badRequest, internalServerError, notFound } from "@/server/errors/next"
+
+import { generateLoyaltyConfigTag } from "@/utils/generateTag"
+
+enum LoyaltyConfigContentTypes {
+ loyalty_level = "loyalty_level",
+ rewards = "rewards",
+}
+
+const validateJsonBody = z.object({
+ data: z.object({
+ content_type: z.object({
+ uid: z.nativeEnum(LoyaltyConfigContentTypes),
+ }),
+ entry: z.object({
+ reward_id: z.string().optional(),
+ level_id: z.string().optional(),
+ locale: z.nativeEnum(Lang),
+ }),
+ }),
+})
+
+export async function POST(request: NextRequest) {
+ try {
+ const headersList = headers()
+ const secret = headersList.get("x-revalidate-secret")
+
+ if (secret !== env.REVALIDATE_SECRET) {
+ console.error(`Invalid Secret`)
+ console.error({ secret })
+ return badRequest({ revalidated: false, now: Date.now() })
+ }
+
+ const data = await request.json()
+ const validatedData = validateJsonBody.safeParse(data)
+ if (!validatedData.success) {
+ console.error(
+ "Bad validation for `validatedData` in loyaltyConfig revalidation"
+ )
+ console.error(validatedData.error)
+ return internalServerError({ revalidated: false, now: Date.now() })
+ }
+
+ const {
+ data: {
+ data: { content_type, entry },
+ },
+ } = validatedData
+
+ let tag = ""
+ if (
+ content_type.uid === LoyaltyConfigContentTypes.loyalty_level &&
+ entry.level_id
+ ) {
+ tag = generateLoyaltyConfigTag(
+ entry.locale,
+ content_type.uid,
+ entry.level_id
+ )
+ } else if (
+ content_type.uid === LoyaltyConfigContentTypes.rewards &&
+ entry.reward_id
+ ) {
+ tag = generateLoyaltyConfigTag(
+ entry.locale,
+ content_type.uid,
+ entry.reward_id
+ )
+ } else {
+ console.error("Invalid content_type")
+ return notFound({ revalidated: false, now: Date.now() })
+ }
+
+ console.info(`Revalidating loyalty config tag: ${tag}`)
+ revalidateTag(tag)
+
+ return Response.json({ revalidated: true, now: Date.now() })
+ } catch (error) {
+ console.error("Failed to revalidate tag(s) for loyalty config")
+ console.error(error)
+ return internalServerError({ revalidated: false, now: Date.now() })
+ }
+}
diff --git a/app/globals.css b/app/globals.css
index 5ec453db0..70606b14d 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -107,6 +107,7 @@
--main-menu-mobile-height: 75px;
--main-menu-desktop-height: 118px;
+ --booking-widget-desktop-height: 95px;
--hotel-page-map-desktop-width: 23.75rem;
/* Z-INDEX */
diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx
index 60e654c1a..134eeaa41 100644
--- a/components/Blocks/CardsGrid.tsx
+++ b/components/Blocks/CardsGrid.tsx
@@ -1,9 +1,9 @@
import SectionContainer from "@/components/Section/Container"
import SectionHeader from "@/components/Section/Header"
import Card from "@/components/TempDesignSystem/Card"
-import ContentCard from "@/components/TempDesignSystem/ContentCard"
import Grids from "@/components/TempDesignSystem/Grids"
import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard"
+import TeaserCard from "@/components/TempDesignSystem/TeaserCard"
import type { CardsGridProps } from "@/types/components/blocks/cardsGrid"
import { CardsGridEnum } from "@/types/enums/cardsGrid"
@@ -22,37 +22,37 @@ export default function CardsGrid({
{cards_grid.cards.map((card) => {
switch (card.__typename) {
- case CardsGridEnum.cards.Card: {
- return card.isContentCard ? (
-
+ )
+ case CardsGridEnum.cards.TeaserCard:
+ return (
+
- ) : (
-
)
- }
case CardsGridEnum.cards.LoyaltyCard:
return (
)
diff --git a/components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx b/components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx
deleted file mode 100644
index 31a920f12..000000000
--- a/components/Blocks/DynamicContent/Benefits/CurrentLevel/index.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { MembershipLevelEnum } from "@/constants/membershipLevels"
-import { getProfile } from "@/lib/trpc/memoizedRequests"
-
-import SectionContainer from "@/components/Section/Container"
-import SectionHeader from "@/components/Section/Header"
-import SectionLink from "@/components/Section/Link"
-import Grids from "@/components/TempDesignSystem/Grids"
-import Title from "@/components/TempDesignSystem/Text/Title"
-import { getLang } from "@/i18n/serverContext"
-import { getMembershipLevelObject } from "@/utils/membershipLevel"
-
-import styles from "./current.module.css"
-
-import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
-
-export default async function CurrentBenefitsBlock({
- title,
- subtitle,
- link,
-}: AccountPageComponentProps) {
- const user = await getProfile()
- // TAKE NOTE: we need clarification on how benefits stack from different levels
- // in order to determine if a benefit is specific to a level or if it is a cumulative benefit
- // we might have to add a new boolean property "exclusive" or similar
- if (!user || "error" in user || !user.membership) {
- return null
- }
-
- const currentLevel = getMembershipLevelObject(
- user.membership.membershipLevel as MembershipLevelEnum,
- getLang()
- )
- if (!currentLevel) {
- // TODO: handle this case?
- return null
- }
-
- return (
-
-
-
- {currentLevel.benefits.map((benefit, idx) => (
-
-
- {benefit.title}
-
-
- ))}
-
-
-
- )
-}
diff --git a/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx b/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx
index 1b3c30154..f23130e7d 100644
--- a/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx
+++ b/components/Blocks/DynamicContent/LoyaltyLevels/index.tsx
@@ -1,88 +1,43 @@
-"use client"
-
-import { notFound, useParams } from "next/navigation"
-import { useIntl } from "react-intl"
-
-import { Lang } from "@/constants/languages"
+import { serverClient } from "@/lib/trpc/server"
import { CheckIcon } from "@/components/Icons"
-import {
- BestFriend,
- CloseFriend,
- DearFriend,
- GoodFriend,
- LoyalFriend,
- NewFriend,
- TrueFriend,
-} from "@/components/Levels"
+import MembershipLevelIcon from "@/components/Levels/Icon"
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
-import levelsData from "@/data/loyaltyLevels"
+import { getIntl } from "@/i18n"
+import { getLang } from "@/i18n/serverContext"
import SectionWrapper from "../SectionWrapper"
import styles from "./loyaltyLevels.module.css"
-import type { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent"
-import type { Level, LevelCardProps } from "@/types/components/overviewTable"
+import { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent"
+import type { LevelCardProps } from "@/types/components/overviewTable"
-export default function LoyaltyLevels({
+export default async function LoyaltyLevels({
dynamic_content,
firstItem,
}: LoyaltyLevelsProps) {
- const params = useParams()
- const lang = params.lang as Lang
- const { formatMessage } = useIntl()
+ const uniqueLevels = await serverClient().contentstack.rewards.all({
+ unique: true,
+ })
- const { levels } = levelsData[lang]
return (
- {levels.map((level: Level) => (
-
+ {uniqueLevels.map((level) => (
+
))}
)
}
-function LevelCard({ formatMessage, lang, level }: LevelCardProps) {
- let Level = null
- switch (level.level) {
- case 1:
- Level = NewFriend
- break
- case 2:
- Level = GoodFriend
- break
- case 3:
- Level = CloseFriend
- break
- case 4:
- Level = DearFriend
- break
- case 5:
- Level = LoyalFriend
- break
- case 6:
- Level = TrueFriend
- break
- case 7:
- Level = BestFriend
- break
- default: {
- const loyaltyLevel = level.level as never
- console.error(`Unsupported loyalty level given: ${loyaltyLevel}`)
- notFound()
- }
- }
- const pointsString = `${level.requiredPoints.toLocaleString(lang)} ${formatMessage({ id: "points" })} `
+async function LevelCard({ level }: LevelCardProps) {
+ const lang = getLang()
+ const intl = await getIntl()
+ const pointsString = `${level.required_points.toLocaleString(lang)} ${intl.formatMessage({ id: "points" })} `
return (
@@ -92,24 +47,24 @@ function LevelCard({ formatMessage, lang, level }: LevelCardProps) {
color="primaryLightOnSurfaceAccent"
tilted="large"
>
- {formatMessage({ id: "Level" })} {level.level}
+ {intl.formatMessage({ id: "Level" })} {level.user_facing_tag}
-
+
{pointsString}
- {level.requiredNights ? (
+ {level.required_nights ? (
- {formatMessage({ id: "or" })} {level.requiredNights}{" "}
- {formatMessage({ id: "nights" })}
+ {intl.formatMessage({ id: "or" })} {level.required_nights}{" "}
+ {intl.formatMessage({ id: "nights" })}
) : null}
- {level.benefits.map((benefit) => (
+ {level.rewards.map((reward) => (
@@ -117,7 +72,7 @@ function LevelCard({ formatMessage, lang, level }: LevelCardProps) {
className={styles.checkIcon}
color="primaryLightOnSurfaceAccent"
/>
- {benefit.title}
+ {reward.label}
))}
diff --git a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx b/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx
deleted file mode 100644
index 07d4743ee..000000000
--- a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { membershipLevels } from "@/constants/membershipLevels"
-
-import {
- BestFriend,
- CloseFriend,
- DearFriend,
- GoodFriend,
- LoyalFriend,
- NewFriend,
- TrueFriend,
-} from "@/components/Levels"
-
-import styles from "./membershipLevel.module.css"
-
-import type { MembershipLevelProps } from "@/types/components/myPages/membership"
-
-export default function MembershipLevel({ level }: MembershipLevelProps) {
- switch (level) {
- case membershipLevels.L1:
- return
- case membershipLevels.L2:
- return
- case membershipLevels.L3:
- return
- case membershipLevels.L4:
- return
- case membershipLevels.L5:
- return
- case membershipLevels.L6:
- return
- case membershipLevels.L7:
- return
- default:
- return null
- }
-}
diff --git a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css b/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css
deleted file mode 100644
index 25b922864..000000000
--- a/components/Blocks/DynamicContent/Overview/Friend/MembershipLevel/membershipLevel.module.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.level {
- height: 105px;
- width: 219px;
-}
diff --git a/components/Blocks/DynamicContent/Overview/Friend/index.tsx b/components/Blocks/DynamicContent/Overview/Friend/index.tsx
index c581765b6..3f1b30ed1 100644
--- a/components/Blocks/DynamicContent/Overview/Friend/index.tsx
+++ b/components/Blocks/DynamicContent/Overview/Friend/index.tsx
@@ -1,12 +1,14 @@
-import { membershipLevels } from "@/constants/membershipLevels"
+import {
+ MembershipLevelEnum,
+ membershipLevels,
+} from "@/constants/membershipLevels"
+import MembershipLevelIcon from "@/components/Levels/Icon"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import { isHighestMembership } from "@/utils/user"
-import MembershipLevel from "./MembershipLevel"
-
import styles from "./friend.module.css"
import type { FriendProps } from "@/types/components/myPages/friend"
@@ -20,7 +22,6 @@ export default async function Friend({
if (!membership?.membershipLevel) {
return null
}
- // @ts-expect-error: membershiplevel needs proper fix
const isHighestLevel = isHighestMembership(membership.membershipLevel)
return (
@@ -30,16 +31,14 @@ export default async function Friend({
{formatMessage(
isHighestLevel
? { id: "Highest level" }
- : // @ts-expect-error: membershiplevel needs proper fix
- { id: `Level ${membershipLevels[membership.membershipLevel]}` }
+ : { id: `Level ${membershipLevels[membership.membershipLevel]}` }
)}