From 84f2bc4c0721ce4a773b94af93a94df515e24b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= Date: Wed, 20 Nov 2024 08:43:55 +0100 Subject: [PATCH 01/58] fix: enable force-cache for CurrentFooter and booking widget toggle query --- server/routers/contentstack/base/query.ts | 1 + server/routers/contentstack/bookingwidget/query.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index b45029eb8..8cdb9b0b9 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -422,6 +422,7 @@ export const baseQueryRouter = router({ locale: input.lang, }, { + cache: "force-cache", next: { tags: [generateTag(input.lang, currentFooterUID)], }, diff --git a/server/routers/contentstack/bookingwidget/query.ts b/server/routers/contentstack/bookingwidget/query.ts index 50af970e1..32292e7e0 100644 --- a/server/routers/contentstack/bookingwidget/query.ts +++ b/server/routers/contentstack/bookingwidget/query.ts @@ -70,6 +70,7 @@ export const bookingwidgetQueryRouter = router({ locale: lang, }, { + cache: "force-cache", next: { tags: [generateTag(lang, uid, bookingwidgetAffix)], }, From b5331670443b09c6ce2da68a9b2f86104eb286e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= Date: Wed, 20 Nov 2024 08:44:23 +0100 Subject: [PATCH 02/58] fix: optimize reward query by fetching loyalty levels concurrently --- server/routers/contentstack/reward/query.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index b1f5ccb8c..7746822ff 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -130,13 +130,15 @@ export const rewardQueryRouter = router({ .map((reward) => reward?.rewardId) .filter((id): id is string => Boolean(id)) - const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds) + const [contentStackRewards, loyaltyLevelsConfig] = await Promise.all([ + getCmsRewards(ctx.lang, rewardIds), + getLoyaltyLevel(ctx, input.level_id), + ]) + if (!contentStackRewards) { return null } - const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id) - const levelsWithRewards = apiRewards .map((reward) => { const contentStackReward = contentStackRewards.find((r) => { From 9f982dcea633e741ca22edd9800ba15995df33e5 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Wed, 20 Nov 2024 12:40:23 +0100 Subject: [PATCH 03/58] feat: sw-929 show booking widget on hotelreservation path and separate env for sitewide alerts --- .env.local.example | 2 ++ .env.test | 2 ++ .../hotelreservation/(standard)/layout.tsx | 5 ++++- .../(standard)/page.module.css | 8 +++++++ .../hotelreservation/(standard)/page.tsx | 15 ++++++++++++- .../hotelreservation/[...paths]/page.tsx | 2 +- .../@bookingwidget/hotelreservation/page.tsx | 22 ++++++++++++++++++- app/[lang]/(live)/@sitewidealert/page.tsx | 2 +- app/[lang]/(live)/layout.tsx | 4 ++-- env/server.ts | 17 ++++++++++++++ 10 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css diff --git a/.env.local.example b/.env.local.example index c6f85bec7..d7516b393 100644 --- a/.env.local.example +++ b/.env.local.example @@ -52,5 +52,7 @@ GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" HIDE_FOR_NEXT_RELEASE="false" +HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +HIDE_SITE_WIDE_ALERT="false" SHOW_SIGNUP_FLOW="true" USE_NEW_REWARDS_ENDPOINT="true" diff --git a/.env.test b/.env.test index f651cbe63..a130c62d5 100644 --- a/.env.test +++ b/.env.test @@ -44,3 +44,5 @@ GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" USE_NEW_REWARDS_ENDPOINT="true" +HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +HIDE_SITE_WIDE_ALERT="false" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx index 21bf78f5e..373cb817b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx @@ -12,7 +12,10 @@ export default function HotelReservationLayout({ }: React.PropsWithChildren> & { sidePeek: React.ReactNode }) { - if (env.HIDE_FOR_NEXT_RELEASE) { + if ( + env.HIDE_FOR_NEXT_RELEASE && + env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH + ) { return notFound() } return ( diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css new file mode 100644 index 000000000..3446c2a84 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css @@ -0,0 +1,8 @@ +.page { + background-color: var(--Base-Background-Primary-Normal); + min-height: 50dvh; + max-width: var(--max-width); + display: flex; + align-items: center; + justify-content: center; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx index 981b0d765..7afaf8599 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx @@ -1,8 +1,21 @@ +import { env } from "@/env/server" + import { setLang } from "@/i18n/serverContext" +import styles from "./page.module.css" + import type { LangParams, PageArgs } from "@/types/params" export default function HotelReservationPage({ params }: PageArgs) { setLang(params.lang) - return null + + if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + return null + } + + return ( +
+ New booking flow! Please report errors/issues in Slack. +
+ ) } diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx index 2ebaca014..03a82e5f5 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx @@ -1 +1 @@ -export { default } from "../../page" +export { default } from "../page" diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx index 03a82e5f5..1a77e08c1 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx @@ -1 +1,21 @@ -export { default } from "../page" +import { env } from "@/env/server" + +import BookingWidget, { preload } from "@/components/BookingWidget" + +import { PageArgs } from "@/types/params" + +export default async function BookingWidgetHotelReservationPage({ + searchParams, +}: PageArgs<{}, URLSearchParams>) { + if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + return null + } + + preload() + + return +} + +// TODO: This should just: +// export { default } from "../page" +// when current web is no more diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index be7ae2256..05796b740 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -8,7 +8,7 @@ import { setLang } from "@/i18n/serverContext" import type { LangParams, PageArgs } from "@/types/params" export default function SitewideAlertPage({ params }: PageArgs) { - if (env.HIDE_FOR_NEXT_RELEASE) { + if (env.HIDE_SITE_WIDE_ALERT) { return null } diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index e1c63bbbc..511e59469 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -57,9 +57,9 @@ export default async function RootLayout({ - {!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}} + {sitewidealert} {header} - {!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}} + {bookingwidget} {children} {footer} diff --git a/env/server.ts b/env/server.ts index 57fc05acf..4e517a146 100644 --- a/env/server.ts +++ b/env/server.ts @@ -79,6 +79,20 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true"), + HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("true"), + HIDE_SITE_WIDE_ALERT: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("true"), USE_NEW_REWARDS_ENDPOINT: z .string() // only allow "true" or "false" @@ -142,5 +156,8 @@ export const env = createEnv({ GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE, USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT, + HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH: + process.env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH, + HIDE_SITE_WIDE_ALERT: process.env.HIDE_SITE_WIDE_ALERT, }, }) From ea325796e3f37f91f3a717d62eec80a3db7dcda8 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Wed, 20 Nov 2024 12:40:23 +0100 Subject: [PATCH 04/58] feat: sw-929 show booking widget on hotelreservation path and separate env for sitewide alerts --- .env.local.example | 2 ++ .env.test | 2 ++ .../hotelreservation/(standard)/layout.tsx | 5 ++++- .../(standard)/page.module.css | 8 +++++++ .../hotelreservation/(standard)/page.tsx | 15 ++++++++++++- .../hotelreservation/[...paths]/page.tsx | 2 +- .../@bookingwidget/hotelreservation/page.tsx | 22 ++++++++++++++++++- app/[lang]/(live)/@sitewidealert/page.tsx | 2 +- app/[lang]/(live)/layout.tsx | 4 ++-- env/server.ts | 17 ++++++++++++++ 10 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css diff --git a/.env.local.example b/.env.local.example index c6f85bec7..d7516b393 100644 --- a/.env.local.example +++ b/.env.local.example @@ -52,5 +52,7 @@ GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" HIDE_FOR_NEXT_RELEASE="false" +HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +HIDE_SITE_WIDE_ALERT="false" SHOW_SIGNUP_FLOW="true" USE_NEW_REWARDS_ENDPOINT="true" diff --git a/.env.test b/.env.test index f651cbe63..a130c62d5 100644 --- a/.env.test +++ b/.env.test @@ -44,3 +44,5 @@ GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" USE_NEW_REWARDS_ENDPOINT="true" +HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +HIDE_SITE_WIDE_ALERT="false" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx index 21bf78f5e..373cb817b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx @@ -12,7 +12,10 @@ export default function HotelReservationLayout({ }: React.PropsWithChildren> & { sidePeek: React.ReactNode }) { - if (env.HIDE_FOR_NEXT_RELEASE) { + if ( + env.HIDE_FOR_NEXT_RELEASE && + env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH + ) { return notFound() } return ( diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css new file mode 100644 index 000000000..3446c2a84 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.module.css @@ -0,0 +1,8 @@ +.page { + background-color: var(--Base-Background-Primary-Normal); + min-height: 50dvh; + max-width: var(--max-width); + display: flex; + align-items: center; + justify-content: center; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx index 981b0d765..7afaf8599 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx @@ -1,8 +1,21 @@ +import { env } from "@/env/server" + import { setLang } from "@/i18n/serverContext" +import styles from "./page.module.css" + import type { LangParams, PageArgs } from "@/types/params" export default function HotelReservationPage({ params }: PageArgs) { setLang(params.lang) - return null + + if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + return null + } + + return ( +
+ New booking flow! Please report errors/issues in Slack. +
+ ) } diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx index 2ebaca014..03a82e5f5 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx @@ -1 +1 @@ -export { default } from "../../page" +export { default } from "../page" diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx index 03a82e5f5..1a77e08c1 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx @@ -1 +1,21 @@ -export { default } from "../page" +import { env } from "@/env/server" + +import BookingWidget, { preload } from "@/components/BookingWidget" + +import { PageArgs } from "@/types/params" + +export default async function BookingWidgetHotelReservationPage({ + searchParams, +}: PageArgs<{}, URLSearchParams>) { + if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + return null + } + + preload() + + return +} + +// TODO: This should just: +// export { default } from "../page" +// when current web is no more diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index be7ae2256..05796b740 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -8,7 +8,7 @@ import { setLang } from "@/i18n/serverContext" import type { LangParams, PageArgs } from "@/types/params" export default function SitewideAlertPage({ params }: PageArgs) { - if (env.HIDE_FOR_NEXT_RELEASE) { + if (env.HIDE_SITE_WIDE_ALERT) { return null } diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index e1c63bbbc..511e59469 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -57,9 +57,9 @@ export default async function RootLayout({ - {!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}} + {sitewidealert} {header} - {!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}} + {bookingwidget} {children} {footer} diff --git a/env/server.ts b/env/server.ts index 57fc05acf..4e517a146 100644 --- a/env/server.ts +++ b/env/server.ts @@ -79,6 +79,20 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true"), + HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("true"), + HIDE_SITE_WIDE_ALERT: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("true"), USE_NEW_REWARDS_ENDPOINT: z .string() // only allow "true" or "false" @@ -142,5 +156,8 @@ export const env = createEnv({ GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE, USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT, + HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH: + process.env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH, + HIDE_SITE_WIDE_ALERT: process.env.HIDE_SITE_WIDE_ALERT, }, }) From ff8a1b836c8e33a766e0e18f87fed315f9cbae52 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Wed, 20 Nov 2024 13:44:34 +0100 Subject: [PATCH 05/58] Fix merge issues --- .../hotelreservation/[...path]/page.tsx | 21 ++++++++++++++++++ app/[lang]/(live)/@footer/[...path]/page.tsx | 22 +------------------ 2 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx new file mode 100644 index 000000000..1a77e08c1 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx @@ -0,0 +1,21 @@ +import { env } from "@/env/server" + +import BookingWidget, { preload } from "@/components/BookingWidget" + +import { PageArgs } from "@/types/params" + +export default async function BookingWidgetHotelReservationPage({ + searchParams, +}: PageArgs<{}, URLSearchParams>) { + if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + return null + } + + preload() + + return +} + +// TODO: This should just: +// export { default } from "../page" +// when current web is no more diff --git a/app/[lang]/(live)/@footer/[...path]/page.tsx b/app/[lang]/(live)/@footer/[...path]/page.tsx index 1a77e08c1..03a82e5f5 100644 --- a/app/[lang]/(live)/@footer/[...path]/page.tsx +++ b/app/[lang]/(live)/@footer/[...path]/page.tsx @@ -1,21 +1 @@ -import { env } from "@/env/server" - -import BookingWidget, { preload } from "@/components/BookingWidget" - -import { PageArgs } from "@/types/params" - -export default async function BookingWidgetHotelReservationPage({ - searchParams, -}: PageArgs<{}, URLSearchParams>) { - if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { - return null - } - - preload() - - return -} - -// TODO: This should just: -// export { default } from "../page" -// when current web is no more +export { default } from "../page" From 367da173fbdfb77b022a089359ab391286b86dcf Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 21 Nov 2024 07:23:11 +0100 Subject: [PATCH 06/58] Inverted flag values --- .env.local.example | 4 ++-- .env.test | 4 ++-- .../hotelreservation/(standard)/layout.tsx | 2 +- .../(public)/hotelreservation/(standard)/page.tsx | 2 +- .../hotelreservation/[...path]/page.tsx | 2 +- app/[lang]/(live)/@sitewidealert/page.tsx | 2 +- env/server.ts | 14 +++++++------- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.env.local.example b/.env.local.example index d7516b393..45b3c3b37 100644 --- a/.env.local.example +++ b/.env.local.example @@ -52,7 +52,7 @@ GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" HIDE_FOR_NEXT_RELEASE="false" -HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" -HIDE_SITE_WIDE_ALERT="false" +SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +SHOW_SITE_WIDE_ALERT="false" SHOW_SIGNUP_FLOW="true" USE_NEW_REWARDS_ENDPOINT="true" diff --git a/.env.test b/.env.test index a130c62d5..4a68e99bb 100644 --- a/.env.test +++ b/.env.test @@ -44,5 +44,5 @@ GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" USE_NEW_REWARDS_ENDPOINT="true" -HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" -HIDE_SITE_WIDE_ALERT="false" +SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +SHOW_SITE_WIDE_ALERT="false" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx index 373cb817b..774b91926 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx @@ -14,7 +14,7 @@ export default function HotelReservationLayout({ }) { if ( env.HIDE_FOR_NEXT_RELEASE && - env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH + !env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH ) { return notFound() } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx index 7afaf8599..44dceff92 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx @@ -9,7 +9,7 @@ import type { LangParams, PageArgs } from "@/types/params" export default function HotelReservationPage({ params }: PageArgs) { setLang(params.lang) - if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + if (!env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH) { return null } diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx index 1a77e08c1..9b483b601 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx @@ -7,7 +7,7 @@ import { PageArgs } from "@/types/params" export default async function BookingWidgetHotelReservationPage({ searchParams, }: PageArgs<{}, URLSearchParams>) { - if (env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + if (!env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH) { return null } diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index d212af549..f96ebbbdb 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -8,7 +8,7 @@ import { setLang } from "@/i18n/serverContext" import type { LangParams, PageArgs } from "@/types/params" export default function SitewideAlertPage({ params }: PageArgs) { - if (env.HIDE_SITE_WIDE_ALERT) { + if (!env.SHOW_SITE_WIDE_ALERT) { return null } diff --git a/env/server.ts b/env/server.ts index 4e517a146..dbe24a894 100644 --- a/env/server.ts +++ b/env/server.ts @@ -79,20 +79,20 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true"), - HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH: z + SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH: z .string() // only allow "true" or "false" .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true") - .default("true"), - HIDE_SITE_WIDE_ALERT: z + .default("false"), + SHOW_SITE_WIDE_ALERT: z .string() // only allow "true" or "false" .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true") - .default("true"), + .default("false"), USE_NEW_REWARDS_ENDPOINT: z .string() // only allow "true" or "false" @@ -156,8 +156,8 @@ export const env = createEnv({ GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE, USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT, - HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH: - process.env.HIDE_BOOKINGWIDGET_HOTELRESERVATION_PATH, - HIDE_SITE_WIDE_ALERT: process.env.HIDE_SITE_WIDE_ALERT, + SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH: + process.env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH, + SHOW_SITE_WIDE_ALERT: process.env.SHOW_SITE_WIDE_ALERT, }, }) From 0633bd427f6283f09bc5ec10b9b238b8df8dd395 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 21 Nov 2024 07:25:38 +0100 Subject: [PATCH 07/58] Fix build error --- next-env.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. From 299da82ba3c0941335706f35796cb66f5110b827 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 21 Nov 2024 07:25:53 +0100 Subject: [PATCH 08/58] Fix build error --- .../hotelreservation/{[...path] => [...paths]}/page.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/[lang]/(live)/@bookingwidget/hotelreservation/{[...path] => [...paths]}/page.tsx (100%) diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx similarity index 100% rename from app/[lang]/(live)/@bookingwidget/hotelreservation/[...path]/page.tsx rename to app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx From de22d873a111311fad5ef6f74c400fa78f092db4 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Fri, 22 Nov 2024 13:25:57 +0100 Subject: [PATCH 09/58] Refactor --- .env.local.example | 6 +++- .env.test | 4 ++- .../hotelreservation/(standard)/layout.tsx | 4 +-- .../hotelreservation/(standard)/page.tsx | 2 +- .../hotelreservation/[...paths]/page.tsx | 28 ++++++++++--------- app/[lang]/(live)/@bookingwidget/page.tsx | 2 +- .../SelectRate/RoomSelection/utils.ts | 2 +- env/server.ts | 22 +++++++++++++-- server/tokenManager.ts | 7 +++-- 9 files changed, 51 insertions(+), 26 deletions(-) diff --git a/.env.local.example b/.env.local.example index 45b3c3b37..36561372b 100644 --- a/.env.local.example +++ b/.env.local.example @@ -52,7 +52,11 @@ GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" HIDE_FOR_NEXT_RELEASE="false" -SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" + +ENABLE_BOOKING_FLOW="false" +ENABLE_BOOKING_WIDGET="false" +ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false" + SHOW_SITE_WIDE_ALERT="false" SHOW_SIGNUP_FLOW="true" USE_NEW_REWARDS_ENDPOINT="true" diff --git a/.env.test b/.env.test index 4a68e99bb..9f7f67b28 100644 --- a/.env.test +++ b/.env.test @@ -44,5 +44,7 @@ GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" USE_NEW_REWARDS_ENDPOINT="true" -SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH="false" +ENABLE_BOOKING_FLOW="false" +ENABLE_BOOKING_WIDGET="false" +ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false" SHOW_SITE_WIDE_ALERT="false" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx index 774b91926..a8aeefed1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx @@ -13,8 +13,8 @@ export default function HotelReservationLayout({ sidePeek: React.ReactNode }) { if ( - env.HIDE_FOR_NEXT_RELEASE && - !env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH + !env.ENABLE_BOOKING_FLOW && + !env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH ) { return notFound() } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx index 44dceff92..ca39e25b8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx @@ -9,7 +9,7 @@ import type { LangParams, PageArgs } from "@/types/params" export default function HotelReservationPage({ params }: PageArgs) { setLang(params.lang) - if (!env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH) { + if (!env.ENABLE_BOOKING_FLOW) { return null } diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx index 9b483b601..1eea8e957 100644 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx @@ -1,21 +1,23 @@ -import { env } from "@/env/server" +// import { env } from "@/env/server" -import BookingWidget, { preload } from "@/components/BookingWidget" +// import BookingWidget, { preload } from "@/components/BookingWidget" -import { PageArgs } from "@/types/params" +// import { PageArgs } from "@/types/params" -export default async function BookingWidgetHotelReservationPage({ - searchParams, -}: PageArgs<{}, URLSearchParams>) { - if (!env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH) { - return null - } +// export default async function BookingWidgetHotelReservationPage({ +// searchParams, +// }: PageArgs<{}, URLSearchParams>) { +// if (!env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH) { +// return null +// } - preload() +// preload() - return -} +// return <>BOOKING WIDGET!!!!! + +// return +// } // TODO: This should just: -// export { default } from "../page" +export { default } from "../../page" // when current web is no more diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 6c944ae69..a2c4b15ca 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -8,7 +8,7 @@ import { PageArgs } from "@/types/params" export default async function BookingWidgetPage({ searchParams, }: PageArgs<{}, URLSearchParams>) { - if (env.HIDE_FOR_NEXT_RELEASE) { + if (!env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH) { return null } diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 43af470e3..2cb36f189 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -44,7 +44,7 @@ export function getQueryParamsForEnterDetails( fromDate: selectRoomParamsObject.fromDate, toDate: selectRoomParamsObject.toDate, hotel: selectRoomParamsObject.hotel, - rooms: room.map((room) => ({ + rooms: room?.map((room) => ({ adults: room.adults, // TODO: Handle multiple rooms children: room.child, // TODO: Handle multiple rooms and children roomTypeCode: room.roomtype, diff --git a/env/server.ts b/env/server.ts index dbe24a894..cbfcf5155 100644 --- a/env/server.ts +++ b/env/server.ts @@ -79,7 +79,21 @@ export const env = createEnv({ .refine((s) => s === "true" || s === "false") // transform to boolean .transform((s) => s === "true"), - SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH: z + ENABLE_BOOKING_FLOW: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("false"), + ENABLE_BOOKING_WIDGET: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("false"), + ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH: z .string() // only allow "true" or "false" .refine((s) => s === "true" || s === "false") @@ -156,8 +170,10 @@ export const env = createEnv({ GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE, USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT, - SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH: - process.env.SHOW_BOOKINGWIDGET_HOTELRESERVATION_PATH, + ENABLE_BOOKING_FLOW: process.env.ENABLE_BOOKING_FLOW, + ENABLE_BOOKING_WIDGET: process.env.ENABLE_BOOKING_WIDGET, + ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH: + process.env.ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH, SHOW_SITE_WIDE_ALERT: process.env.SHOW_SITE_WIDE_ALERT, }, }) diff --git a/server/tokenManager.ts b/server/tokenManager.ts index 980ca071d..d16faea74 100644 --- a/server/tokenManager.ts +++ b/server/tokenManager.ts @@ -71,11 +71,12 @@ async function fetchServiceToken(scopes: string[]) { export async function getServiceToken() { let scopes: string[] = [] - if (env.HIDE_FOR_NEXT_RELEASE) { - scopes = ["profile"] - } else { + if (env.ENABLE_BOOKING_FLOW) { scopes = ["profile", "hotel", "booking", "package"] + } else { + scopes = ["profile"] } + const tag = generateServiceTokenTag(scopes) const getCachedJwt = unstable_cache( async (scopes) => { From 333be1379cc7f0dcd8bebbfb527d054dbdebea94 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 22 Nov 2024 17:05:29 +0100 Subject: [PATCH 10/58] fix(SW-983): Fixed bug with hotel card in map --- .../HotelReservation/HotelCard/index.tsx | 14 +++-- .../hotelCardDialog.module.css | 7 ++- .../hotelCardDialogListing.module.css | 20 ++++++ .../HotelCardDialogListing/index.tsx | 15 ++++- .../HotelListing/hotelListing.module.css | 19 +----- components/ImageGallery/index.tsx | 6 +- .../HotelListingMapContent/index.tsx | 61 +++++++++++-------- 7 files changed, 88 insertions(+), 54 deletions(-) create mode 100644 components/HotelReservation/HotelCardDialogListing/hotelCardDialogListing.module.css diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 916796229..7789f8b2d 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,5 +1,6 @@ "use client" import { useParams } from "next/dist/client/components/navigation" +import { memo, useCallback } from "react" import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" @@ -24,7 +25,7 @@ import styles from "./hotelCard.module.css" import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" -export default function HotelCard({ +function HotelCard({ hotel, type = HotelCardListingTypeEnum.PageListing, state = "default", @@ -44,16 +45,17 @@ export default function HotelCard({ state, }) - const handleMouseEnter = () => { + const handleMouseEnter = useCallback(() => { if (onHotelCardHover) { onHotelCardHover(hotelData.name) } - } - const handleMouseLeave = () => { + }, [onHotelCardHover, hotelData.name]) + + const handleMouseLeave = useCallback(() => { if (onHotelCardHover) { onHotelCardHover(null) } - } + }, [onHotelCardHover]) return (
) } + +export default memo(HotelCard) diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css index 5c6e6d6bd..7d607d3ad 100644 --- a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -1,6 +1,6 @@ .dialog { padding-bottom: var(--Spacing-x1); - bottom: 32px; + bottom: 0; left: 50%; transform: translateX(-50%); border: none; @@ -33,6 +33,8 @@ .imageContainer { position: relative; min-width: 177px; + border-radius: var(--Corner-radius-Medium) 0 0 var(--Corner-radius-Medium); + overflow: hidden; } .imageContainer img { @@ -108,4 +110,7 @@ .memberPrice { display: none; } + .dialog { + bottom: 32px; + } } diff --git a/components/HotelReservation/HotelCardDialogListing/hotelCardDialogListing.module.css b/components/HotelReservation/HotelCardDialogListing/hotelCardDialogListing.module.css new file mode 100644 index 000000000..b507aace1 --- /dev/null +++ b/components/HotelReservation/HotelCardDialogListing/hotelCardDialogListing.module.css @@ -0,0 +1,20 @@ +.hotelCardDialogListing { + display: flex; + flex-direction: row; + gap: var(--Spacing-x1); + align-items: flex-end; +} + +.hotelCardDialogListing dialog { + position: relative; + padding: 0; + margin: 0; +} + +.hotelCardDialogListing > div:first-child { + margin-left: 16px; +} + +.hotelCardDialogListing > div:last-child { + margin-right: 16px; +} diff --git a/components/HotelReservation/HotelCardDialogListing/index.tsx b/components/HotelReservation/HotelCardDialogListing/index.tsx index f13ff0433..123cc4ced 100644 --- a/components/HotelReservation/HotelCardDialogListing/index.tsx +++ b/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -1,10 +1,15 @@ "use client" import { useCallback, useEffect, useRef } from "react" +import { useMediaQuery } from "usehooks-ts" + +import useClickOutside from "@/hooks/useClickOutside" import HotelCardDialog from "../HotelCardDialog" import { getHotelPins } from "./utils" +import styles from "./hotelCardDialogListing.module.css" + import type { HotelCardDialogListingProps } from "@/types/components/hotelReservation/selectHotel/map" export default function HotelCardDialogListing({ @@ -15,6 +20,12 @@ export default function HotelCardDialogListing({ const hotelsPinData = getHotelPins(hotels) const activeCardRef = useRef(null) const observerRef = useRef(null) + const dialogRef = useRef(null) + const isMobile = useMediaQuery("(max-width: 768px)") + + useClickOutside(dialogRef, !!activeCard && isMobile, () => { + onActiveCardChange(null) + }) const handleIntersection = useCallback( (entries: IntersectionObserverEntry[]) => { @@ -65,7 +76,7 @@ export default function HotelCardDialogListing({ }, [activeCard]) return ( - <> +
{hotelsPinData?.length && hotelsPinData.map((data) => { const isActive = data.name === activeCard @@ -83,6 +94,6 @@ export default function HotelCardDialogListing({
) })} - + ) } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css index 5fd7e4084..401c7fe3a 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css @@ -4,35 +4,18 @@ .hotelListingMobile { display: none; - align-items: flex-end; overflow-x: auto; position: absolute; - bottom: 0px; + bottom: 32px; left: 0; right: 0; z-index: 10; - height: 100%; - gap: var(--Spacing-x1); } .hotelListingMobile[data-open="true"] { display: flex; } -.hotelListingMobile dialog { - position: relative; - padding: 0; - margin: 0; -} - -.hotelListingMobile > div:first-child { - margin-left: 16px; -} - -.hotelListingMobile > div:last-child { - margin-right: 16px; -} - @media (min-width: 768px) { .hotelListing { display: block; diff --git a/components/ImageGallery/index.tsx b/components/ImageGallery/index.tsx index 3fc10448b..d26ed0007 100644 --- a/components/ImageGallery/index.tsx +++ b/components/ImageGallery/index.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { memo, useState } from "react" import { useIntl } from "react-intl" import { GalleryIcon } from "@/components/Icons" @@ -12,7 +12,7 @@ import styles from "./imageGallery.module.css" import type { ImageGalleryProps } from "@/types/components/imageGallery" -export default function ImageGallery({ +function ImageGallery({ images, title, fill, @@ -58,3 +58,5 @@ export default function ImageGallery({ ) } + +export default memo(ImageGallery) diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx index c1a3e8c01..9f4c189c0 100644 --- a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx +++ b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx @@ -2,11 +2,10 @@ import { AdvancedMarker, AdvancedMarkerAnchorPoint, } from "@vis.gl/react-google-maps" -import { useRef, useState } from "react" +import { memo, useCallback, useState } from "react" import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog" import Body from "@/components/TempDesignSystem/Text/Body" -import useClickOutside from "@/hooks/useClickOutside" import HotelMarker from "../../Markers/HotelMarker" @@ -14,33 +13,45 @@ import styles from "./hotelListingMapContent.module.css" import type { HotelListingMapContentProps } from "@/types/components/hotelReservation/selectHotel/map" -export default function HotelListingMapContent({ +function HotelListingMapContent({ activeHotelPin, hotelPins, onActiveHotelPinChange, }: HotelListingMapContentProps) { const [hoveredHotelPin, setHoveredHotelPin] = useState(null) - const dialogRef = useRef(null) - function toggleActiveHotelPin(pinName: string | null) { - if (onActiveHotelPinChange) { - onActiveHotelPinChange(activeHotelPin === pinName ? null : pinName) - setHoveredHotelPin(null) - } - } + const toggleActiveHotelPin = useCallback( + (pinName: string | null) => { + if (onActiveHotelPinChange) { + const newActivePin = activeHotelPin === pinName ? null : pinName + onActiveHotelPinChange(newActivePin) + if (newActivePin === null) { + setHoveredHotelPin(null) + } + } + }, + [activeHotelPin, onActiveHotelPinChange] + ) - function isPinActiveOrHovered(pinName: string) { - return activeHotelPin === pinName || hoveredHotelPin === pinName - } - - useClickOutside(dialogRef, isPinActiveOrHovered(activeHotelPin ?? ""), () => { - toggleActiveHotelPin(null) - }) + const handleHover = useCallback( + (pinName: string | null) => { + if (pinName !== null && activeHotelPin !== pinName) { + setHoveredHotelPin(pinName) + if (activeHotelPin && onActiveHotelPinChange) { + onActiveHotelPinChange(null) + } + } else if (pinName === null) { + setHoveredHotelPin(null) + } + }, + [activeHotelPin, onActiveHotelPinChange] + ) return (
{hotelPins.map((pin) => { - const isActiveOrHovered = isPinActiveOrHovered(pin.name) + const isActiveOrHovered = + activeHotelPin === pin.name || hoveredHotelPin === pin.name return ( setHoveredHotelPin(pin.name)} - onMouseLeave={() => setHoveredHotelPin(null)} - onClick={() => { - toggleActiveHotelPin( - activeHotelPin === pin.name ? null : pin.name - ) - }} + onMouseEnter={() => handleHover(pin.name)} + onMouseLeave={() => handleHover(null)} + onClick={() => toggleActiveHotelPin(pin.name)} > -
+
void }) => { @@ -92,3 +99,5 @@ export default function HotelListingMapContent({
) } + +export default memo(HotelListingMapContent) From d612aa83d82067846ddbae927de36049512d3149 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Mon, 25 Nov 2024 10:51:03 +0100 Subject: [PATCH 11/58] fix(SW-992): center empty stays text --- .../Stays/Soonest/EmptyUpcomingStays/index.tsx | 8 +++++++- .../Stays/Upcoming/EmptyUpcomingStays/index.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx index 3835ae61d..fee95429c 100644 --- a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx @@ -14,7 +14,13 @@ export default async function EmptyUpcomingStaysBlock() { return (
- + <Title + as="h4" + level="h3" + color="red" + className={styles.title} + textAlign="center" + > {intl.formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {intl.formatMessage({ id: "Where should you go next?" })} diff --git a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx index 3835ae61d..fee95429c 100644 --- a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx @@ -14,7 +14,13 @@ export default async function EmptyUpcomingStaysBlock() { return ( <section className={styles.container}> <div className={styles.titleContainer}> - <Title as="h4" level="h3" color="red" className={styles.title}> + <Title + as="h4" + level="h3" + color="red" + className={styles.title} + textAlign="center" + > {intl.formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {intl.formatMessage({ id: "Where should you go next?" })} From bd6fd62d5ca167afb0e96ad568d0d30b5e3e0a81 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar <hrishikesh.vaipurkar@scandichotels.com> Date: Fri, 22 Nov 2024 15:02:35 +0100 Subject: [PATCH 12/58] fix: SW-981 Fixed distance to center value --- .../hotelreservation/(standard)/step/@hotelHeader/page.tsx | 2 +- components/ContentType/ContentPage/HotelListingItem/index.tsx | 2 +- components/ContentType/HotelPage/IntroSection/index.tsx | 2 +- components/HotelReservation/HotelCard/index.tsx | 2 +- components/HotelReservation/SelectRate/HotelInfoCard/index.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx index 83412f1d1..0543c5416 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx @@ -47,7 +47,7 @@ export default async function HotelHeader({ <Caption color="textMediumContrast"> {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: hotel.location.distanceToCentre } + { number: hotel.location.distanceToCentre / 1000 } )} </Caption> </address> diff --git a/components/ContentType/ContentPage/HotelListingItem/index.tsx b/components/ContentType/ContentPage/HotelListingItem/index.tsx index 2f8924f1b..d1e592f90 100644 --- a/components/ContentType/ContentPage/HotelListingItem/index.tsx +++ b/components/ContentType/ContentPage/HotelListingItem/index.tsx @@ -47,7 +47,7 @@ export default async function HotelListingItem({ <Caption color="uiTextPlaceholder"> {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: distanceToCentre } + { number: distanceToCentre / 1000 } )} </Caption> </div> diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index 453f1e192..394566fea 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -25,7 +25,7 @@ export default async function IntroSection({ const { distanceToCentre } = location const formattedDistanceText = intl.formatMessage( { id: "Distance in km to city centre" }, - { number: distanceToCentre } + { number: distanceToCentre / 1000 } ) const lang = getLang() const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})` diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 916796229..734035c05 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -104,7 +104,7 @@ export default function HotelCard({ <Caption color="uiTextPlaceholder"> {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: hotelData.location.distanceToCentre } + { number: hotelData.location.distanceToCentre / 1000 } )} </Caption> </div> diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 283a026a6..72abaffc0 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -69,7 +69,7 @@ export default async function HotelInfoCard({
- {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre / 1000} ${intl.formatMessage({ id: "km to city center" })}`} {hotelAttributes.hotelContent.texts.descriptions.medium} From 21f97190503c76dc41de6e679f47f8babf7244ad Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Fri, 22 Nov 2024 18:51:11 +0100 Subject: [PATCH 13/58] fix: SW-981 Rounded to single decimal as current web --- .../(standard)/step/@hotelHeader/page.tsx | 7 ++++++- .../ContentType/ContentPage/HotelListingItem/index.tsx | 3 ++- components/ContentType/HotelPage/IntroSection/index.tsx | 3 ++- components/HotelReservation/HotelCard/index.tsx | 7 ++++++- .../HotelReservation/SelectRate/HotelInfoCard/index.tsx | 3 ++- utils/numberFormatting.ts | 8 ++++++++ 6 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 utils/numberFormatting.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx index 0543c5416..2fafe2dbc 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx @@ -7,6 +7,7 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import getSingleDecimal from "@/utils/numberFormatting" import styles from "./page.module.css" @@ -47,7 +48,11 @@ export default async function HotelHeader({ {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: hotel.location.distanceToCentre / 1000 } + { + number: getSingleDecimal( + hotel.location.distanceToCentre / 1000 + ), + } )} diff --git a/components/ContentType/ContentPage/HotelListingItem/index.tsx b/components/ContentType/ContentPage/HotelListingItem/index.tsx index d1e592f90..18f8f6f7f 100644 --- a/components/ContentType/ContentPage/HotelListingItem/index.tsx +++ b/components/ContentType/ContentPage/HotelListingItem/index.tsx @@ -8,6 +8,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import getSingleDecimal from "@/utils/numberFormatting" import styles from "./hotelListingItem.module.css" @@ -47,7 +48,7 @@ export default async function HotelListingItem({ {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: distanceToCentre / 1000 } + { number: getSingleDecimal(distanceToCentre / 1000) } )}
diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index 394566fea..7a5349ff0 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -8,6 +8,7 @@ import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import getSingleDecimal from "@/utils/numberFormatting" import styles from "./introSection.module.css" @@ -25,7 +26,7 @@ export default async function IntroSection({ const { distanceToCentre } = location const formattedDistanceText = intl.formatMessage( { id: "Distance in km to city centre" }, - { number: distanceToCentre / 1000 } + { number: getSingleDecimal(distanceToCentre / 1000) } ) const lang = getLang() const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})` diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 734035c05..616ca03cf 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -12,6 +12,7 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import getSingleDecimal from "@/utils/numberFormatting" import ReadMore from "../ReadMore" import TripAdvisorChip from "../TripAdvisorChip" @@ -104,7 +105,11 @@ export default function HotelCard({ {intl.formatMessage( { id: "Distance in km to city centre" }, - { number: hotelData.location.distanceToCentre / 1000 } + { + number: getSingleDecimal( + hotelData.location.distanceToCentre / 1000 + ), + } )}
diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 72abaffc0..e7951c490 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -11,6 +11,7 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" +import getSingleDecimal from "@/utils/numberFormatting" import ReadMore from "../../ReadMore" import TripAdvisorChip from "../../TripAdvisorChip" @@ -69,7 +70,7 @@ export default async function HotelInfoCard({
- {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre / 1000} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${getSingleDecimal(hotelAttributes.location.distanceToCentre / 1000)} ${intl.formatMessage({ id: "km to city center" })}`} {hotelAttributes.hotelContent.texts.descriptions.medium} diff --git a/utils/numberFormatting.ts b/utils/numberFormatting.ts new file mode 100644 index 000000000..cd3cd1fa4 --- /dev/null +++ b/utils/numberFormatting.ts @@ -0,0 +1,8 @@ +/** + * Function to parse number with single decimal if any + * @param n + * @returns number in float type with single digit decimal if any + */ +export default function getSingleDecimal(n: Number | string) { + return parseFloat(Number(n).toFixed(1)) +} From 0283c777cade02893f8fec001b810cf030efc9a6 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 25 Nov 2024 13:27:19 +0100 Subject: [PATCH 14/58] fix(SW-984): Scroll to active card (desktop) --- .../HotelCardListing/hotelCardListing.module.css | 2 ++ .../HotelReservation/HotelCardListing/index.tsx | 16 ++++++++++------ .../SelectHotel/SelectHotelMap/index.tsx | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/components/HotelReservation/HotelCardListing/hotelCardListing.module.css b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css index 00bd7ec84..5a5d0ea7e 100644 --- a/components/HotelReservation/HotelCardListing/hotelCardListing.module.css +++ b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css @@ -3,4 +3,6 @@ flex-direction: column; gap: var(--Spacing-x2); margin-bottom: var(--Spacing-x2); + max-height: 100vh; + overflow-y: auto; } diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index f666c5ba2..d9d0b4f30 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -109,13 +109,17 @@ export default function HotelCardListing({
{hotels?.length ? ( hotels.map((hotel) => ( - + data-active={hotel.hotelData.name === activeCard ? "true" : "false"} + > + +
)) ) : activeFilters ? ( (null) const [showBackToTop, setShowBackToTop] = useState(false) + const listingContainerRef = useRef(null) const selectHotelParams = new URLSearchParams(searchParams.toString()) const selectedHotel = selectHotelParams.get("selectedHotel") @@ -43,6 +44,16 @@ export default function SelectHotelMap({ ? cityCoordinates : { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 } + useEffect(() => { + if (listingContainerRef.current) { + const activeElement = + listingContainerRef.current.querySelector(`[data-active="true"]`) + if (activeElement) { + activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" }) + } + } + }, [activeHotelPin]) + useEffect(() => { if (selectedHotel) { setActiveHotelPin(selectedHotel) @@ -90,7 +101,7 @@ export default function SelectHotelMap({ return (
-
+
- - ) : ( - - )} -
- ) -} diff --git a/components/HotelReservation/HotelCard/HotelPriceList/NoPriceAvailableCard/index.tsx b/components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx similarity index 91% rename from components/HotelReservation/HotelCard/HotelPriceList/NoPriceAvailableCard/index.tsx rename to components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx index 38a0e50b3..884b0ec12 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/NoPriceAvailableCard/index.tsx +++ b/components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx @@ -3,7 +3,7 @@ import { useIntl } from "react-intl" import { ErrorCircleIcon } from "@/components/Icons" import Body from "@/components/TempDesignSystem/Text/Body" -import styles from "../hotelPriceList.module.css" +import styles from "./noPriceAvailable.module.css" export default function NoPriceAvailableCard() { const intl = useIntl() diff --git a/components/HotelReservation/HotelCard/NoPriceAvailableCard/noPriceAvailable.module.css b/components/HotelReservation/HotelCard/NoPriceAvailableCard/noPriceAvailable.module.css new file mode 100644 index 000000000..6f3124b12 --- /dev/null +++ b/components/HotelReservation/HotelCard/NoPriceAvailableCard/noPriceAvailable.module.css @@ -0,0 +1,12 @@ +.priceCard { + padding: var(--Spacing-x-one-and-half); + background-color: var(--Base-Surface-Secondary-light-Normal); + border-radius: var(--Corner-radius-Medium); + margin: 0; + width: 100%; +} + +.noRooms { + display: flex; + gap: var(--Spacing-x1); +} diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 089f88800..1d5c651cc 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -70,10 +70,6 @@ gap: var(--Spacing-x-half); } -.detailsButton { - border-bottom: none; -} - .button { min-width: 160px; } @@ -84,6 +80,12 @@ gap: var(--Spacing-x1); } +.prices { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + @media screen and (min-width: 1367px) { .card.pageListing { flex-direction: row; @@ -133,4 +135,8 @@ .pageListing .address { display: inline; } + + .pageListing .prices { + width: 260px; + } } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 1362b3ad6..f6fbbcf12 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -4,10 +4,11 @@ import { memo, useCallback } from "react" import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" -import { selectHotelMap } from "@/constants/routes/hotelReservation" +import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" +import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" @@ -18,7 +19,8 @@ import getSingleDecimal from "@/utils/numberFormatting" import ReadMore from "../ReadMore" import TripAdvisorChip from "../TripAdvisorChip" import HotelLogo from "./HotelLogo" -import HotelPriceList from "./HotelPriceList" +import HotelPriceCard from "./HotelPriceCard" +import NoPriceAvailableCard from "./NoPriceAvailableCard" import { hotelCardVariants } from "./variants" import styles from "./hotelCard.module.css" @@ -139,7 +141,38 @@ function HotelCard({ showCTA={true} />
- +
+ {!price ? ( + + ) : ( + <> + {price.public && ( + + )} + {price.member && ( + + )} + + + )} +
) diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index 3fdcd6344..e6a5e2f02 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -17,7 +17,7 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import NoPriceAvailableCard from "../HotelCard/HotelPriceList/NoPriceAvailableCard" +import NoPriceAvailableCard from "../HotelCard/NoPriceAvailableCard" import styles from "./hotelCardDialog.module.css" diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index 48c73cac2..f2ae9d57f 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -6,7 +6,6 @@ import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { z } from "zod" -import { InfoCircleIcon } from "@/components/Icons" import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -78,10 +77,14 @@ export default function RoomFilter({
- + {intl.formatMessage({ id: "Filter" })} - + {Object.entries(selectedFilters) .filter(([_, value]) => value) .map(([key]) => intl.formatMessage({ id: key })) diff --git a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css index 4af90296c..f94858248 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css +++ b/components/HotelReservation/SelectRate/RoomFilter/roomFilter.module.css @@ -16,7 +16,8 @@ display: flex; flex-direction: row; gap: var(--Spacing-x-half); - align-items: flex-end; + flex-wrap: wrap; + margin-right: var(--Spacing-x1); } .infoDesktop { diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css index 80da80184..a38298d27 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css @@ -46,6 +46,13 @@ input[type="radio"]:checked + .card .checkIcon { .header { display: flex; gap: var(--Spacing-x-half); + align-items: flex-start; +} + +.priceType { + display: flex; + gap: var(--Spacing-x-half); + flex-wrap: wrap; } .button { diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index a79481dcf..e44c3312e 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -89,8 +89,10 @@ export default function FlexibilityOption({ ))} - {name} - ({paymentTerm}) +
+ {name} + ({paymentTerm}) +
pkg.code === RoomPackageCodeEnum.PET_ROOM)) || undefined - const selectedRoom = roomCategories.find( - (room) => room.name === roomConfiguration.roomType + const selectedRoom = roomCategories.find((roomCategory) => + roomCategory.roomTypes.some( + (roomType) => roomType.code === roomConfiguration.roomTypeCode + ) ) - const { roomSize, occupancy, images } = selectedRoom || {} + const { name, roomSize, occupancy, images } = selectedRoom || {} const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) @@ -174,7 +176,7 @@ export default function RoomCard({
- {roomConfiguration.roomType} + {name} {/* Out of scope for now {descriptions?.short} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index b05bbdb8b..9847ba98b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -20,9 +20,9 @@ display: flex; flex-direction: row; align-items: center; + justify-content: space-between; gap: var(--Spacing-x1); padding: 0 var(--Spacing-x1) 0 var(--Spacing-x-one-and-half); - height: 40px; } .specification .guests { @@ -34,6 +34,10 @@ margin-left: auto; } +.toggleSidePeek button { + text-align: start; +} + .container { padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x2); display: flex; diff --git a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css index 2e1fb5e6f..6fc7a47f7 100644 --- a/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css +++ b/components/SidePeeks/RoomSidePeek/roomSidePeek.module.css @@ -50,6 +50,10 @@ margin-bottom: var(--Spacing-x-half); } +.facilityList li svg { + flex-shrink: 0; +} + .noIcon { margin-left: var(--Spacing-x4); } From 70000afe1ff4436ac587707cce55f1ade3aea5ba Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Tue, 26 Nov 2024 09:06:41 +0000 Subject: [PATCH 22/58] Merged in feat/SW-755-price-change-non-happy (pull request #957) Feat/SW-755 price change non happy * fix(SW-755): dont show field error if checkbox has no children * feat(SW-755): Price change route + dialog WIP * fix(SW-755): minor refactoring * fix(SW-755): added logging to price change route * fix(SW-755): remove redundant search param logic * fix(SW-755): moved enum cast to zod instead * fix(SW-755): move prop type to types folder * fix(SW-755): Added suspense to Payment and refactored payment options hook * fix(SW-755): seperated terms and conditions copy from the checkbox label * fix(SW-755): add currency format and fixed wrong translation * fix(SW-755): change from undefined to null * fix(SW-755): added extra type safety to payment options Approved-by: Christian Andolf Approved-by: Simon.Emanuelsson --- .../hotelreservation/(standard)/step/page.tsx | 23 +- .../EnterDetails/Payment/index.tsx | 397 ++++++++++-------- .../EnterDetails/PriceChangeDialog/index.tsx | 73 ++++ .../priceChangeDialog.module.css | 85 ++++ .../Form/Checkbox/checkbox.module.css | 1 + hooks/booking/useAvailablePaymentOptions.ts | 23 + hooks/booking/useHandleBookingStatus.ts | 4 +- hooks/booking/usePaymentFailedToast.ts | 4 +- i18n/dictionaries/da.json | 5 + i18n/dictionaries/de.json | 7 +- i18n/dictionaries/en.json | 5 + i18n/dictionaries/fi.json | 5 + i18n/dictionaries/no.json | 5 + i18n/dictionaries/sv.json | 5 + lib/api/endpoints.ts | 3 + lib/api/index.ts | 51 +-- server/routers/booking/input.ts | 4 + server/routers/booking/mutation.ts | 75 +++- server/routers/booking/output.ts | 4 +- server/routers/hotels/output.ts | 3 +- .../enterDetails/priceChangeDialog.ts | 8 + .../hotelReservation/selectRate/section.ts | 4 +- 22 files changed, 577 insertions(+), 217 deletions(-) create mode 100644 components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx create mode 100644 components/HotelReservation/EnterDetails/PriceChangeDialog/priceChangeDialog.module.css create mode 100644 hooks/booking/useAvailablePaymentOptions.ts create mode 100644 types/components/hotelReservation/enterDetails/priceChangeDialog.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index 4fbaae7ff..ade5f7c8f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,6 +1,7 @@ import "./enterDetailsLayout.css" import { notFound } from "next/navigation" +import { Suspense } from "react" import { getBreakfastPackages, @@ -170,16 +171,18 @@ export default async function StepPage({ step={StepEnum.payment} label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} > - + + + diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index ab1f78807..9adf47149 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -2,8 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useRouter, useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { Label as AriaLabel } from "react-aria-components" +import { useCallback, useEffect, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" @@ -16,6 +15,7 @@ import { bookingTermsAndConditions, privacyPolicy, } from "@/constants/currentWebHrefs" +import { selectRate } from "@/constants/routes/hotelReservation" import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" import { useDetailsStore } from "@/stores/details" @@ -27,11 +27,13 @@ import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { toast } from "@/components/TempDesignSystem/Toasts" +import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast" import useLang from "@/hooks/useLang" import { bedTypeMap } from "../../SelectRate/RoomSelection/utils" +import PriceChangeDialog from "../PriceChangeDialog" import GuaranteeDetails from "./GuaranteeDetails" import PaymentOption from "./PaymentOption" import { PaymentFormData, paymentSchema } from "./schema" @@ -60,29 +62,23 @@ export default function Payment({ const router = useRouter() const lang = useLang() const intl = useIntl() + const searchParams = useSearchParams() const { booking, ...userData } = useDetailsStore((state) => state.data) + const totalPrice = useDetailsStore((state) => state.totalPrice) const setIsSubmittingDisabled = useDetailsStore( (state) => state.actions.setIsSubmittingDisabled ) - const { - firstName, - lastName, - email, - phoneNumber, - countryCode, - breakfast, - bedType, - membershipNo, - join, - dateOfBirth, - zipCode, - } = userData - const { toDate, fromDate, rooms, hotel } = booking - const [confirmationNumber, setConfirmationNumber] = useState("") - const [availablePaymentOptions, setAvailablePaymentOptions] = - useState(otherPaymentOptions) + const [isPollingForBookingStatus, setIsPollingForBookingStatus] = + useState(false) + + const availablePaymentOptions = + useAvailablePaymentOptions(otherPaymentOptions) + const [priceChangeData, setPriceChangeData] = useState<{ + oldPrice: number + newPrice: number + } | null>() usePaymentFailedToast() @@ -103,6 +99,15 @@ export default function Payment({ onSuccess: (result) => { if (result?.confirmationNumber) { setConfirmationNumber(result.confirmationNumber) + + if (result.metadata?.priceChangedMetadata) { + setPriceChangeData({ + oldPrice: roomPrice.publicPrice, + newPrice: result.metadata.priceChangedMetadata.totalPrice, + }) + } else { + setIsPollingForBookingStatus(true) + } } else { toast.error( intl.formatMessage({ @@ -121,25 +126,31 @@ export default function Payment({ }, }) + const priceChange = trpc.booking.priceChange.useMutation({ + onSuccess: (result) => { + if (result?.confirmationNumber) { + setIsPollingForBookingStatus(true) + } else { + toast.error(intl.formatMessage({ id: "payment.error.failed" })) + } + + setPriceChangeData(null) + }, + onError: (error) => { + console.error("Error", error) + setPriceChangeData(null) + toast.error(intl.formatMessage({ id: "payment.error.failed" })) + }, + }) + const bookingStatus = useHandleBookingStatus({ confirmationNumber, expectedStatus: BookingStatusEnum.BookingCompleted, maxRetries, retryInterval, + enabled: isPollingForBookingStatus, }) - useEffect(() => { - if (window.ApplePaySession) { - setAvailablePaymentOptions(otherPaymentOptions) - } else { - setAvailablePaymentOptions( - otherPaymentOptions.filter( - (option) => option !== PaymentMethodEnum.applePay - ) - ) - } - }, [otherPaymentOptions, setAvailablePaymentOptions]) - useEffect(() => { if (bookingStatus?.data?.paymentUrl) { router.push(bookingStatus.data.paymentUrl) @@ -162,76 +173,102 @@ export default function Payment({ setIsSubmittingDisabled, ]) - function handleSubmit(data: PaymentFormData) { - // set payment method to card if saved card is submitted - const paymentMethod = isPaymentMethodEnum(data.paymentMethod) - ? data.paymentMethod - : PaymentMethodEnum.card + const handleSubmit = useCallback( + (data: PaymentFormData) => { + const { + firstName, + lastName, + email, + phoneNumber, + countryCode, + breakfast, + bedType, + membershipNo, + join, + dateOfBirth, + zipCode, + } = userData + const { toDate, fromDate, rooms, hotel } = booking - const savedCreditCard = savedCreditCards?.find( - (card) => card.id === data.paymentMethod - ) + // set payment method to card if saved card is submitted + const paymentMethod = isPaymentMethodEnum(data.paymentMethod) + ? data.paymentMethod + : PaymentMethodEnum.card - const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback` + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) - initiateBooking.mutate({ - hotelId: hotel, - checkInDate: fromDate, - checkOutDate: toDate, - rooms: rooms.map((room) => ({ - adults: room.adults, - childrenAges: room.children?.map((child) => ({ - age: child.age, - bedType: bedTypeMap[parseInt(child.bed.toString())], + const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback` + + initiateBooking.mutate({ + hotelId: hotel, + checkInDate: fromDate, + checkOutDate: toDate, + rooms: rooms.map((room) => ({ + adults: room.adults, + childrenAges: room.children?.map((child) => ({ + age: child.age, + bedType: bedTypeMap[parseInt(child.bed.toString())], + })), + rateCode: + user || join || membershipNo ? room.counterRateCode : room.rateCode, + roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. + guest: { + firstName, + lastName, + email, + phoneNumber, + countryCode, + membershipNumber: membershipNo, + becomeMember: join, + dateOfBirth, + postalCode: zipCode, + }, + packages: { + breakfast: !!(breakfast && breakfast.code), + allergyFriendly: + room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? + false, + petFriendly: + room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false, + accessibility: + room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ?? + false, + }, + smsConfirmationRequested: data.smsConfirmation, + roomPrice, })), - rateCode: - user || join || membershipNo ? room.counterRateCode : room.rateCode, - roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. - guest: { - title: "", - firstName, - lastName, - email, - phoneNumber, - countryCode, - membershipNumber: membershipNo, - becomeMember: join, - dateOfBirth, - postalCode: zipCode, - }, - packages: { - breakfast: !!(breakfast && breakfast.code), - allergyFriendly: - room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, - petFriendly: - room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false, - accessibility: - room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ?? - false, - }, - smsConfirmationRequested: data.smsConfirmation, - roomPrice, - })), - payment: { - paymentMethod, - card: savedCreditCard - ? { - alias: savedCreditCard.alias, - expiryDate: savedCreditCard.expirationDate, - cardType: savedCreditCard.cardType, - } - : undefined, + payment: { + paymentMethod, + card: savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined, - success: `${paymentRedirectUrl}/success`, - error: `${paymentRedirectUrl}/error`, - cancel: `${paymentRedirectUrl}/cancel`, - }, - }) - } + success: `${paymentRedirectUrl}/success`, + error: `${paymentRedirectUrl}/error`, + cancel: `${paymentRedirectUrl}/cancel`, + }, + }) + }, + [ + userData, + booking, + roomPrice, + savedCreditCards, + lang, + user, + initiateBooking, + ] + ) if ( initiateBooking.isPending || - (confirmationNumber && !bookingStatus.data?.paymentUrl) + (isPollingForBookingStatus && !bookingStatus.data?.paymentUrl) ) { return } @@ -241,79 +278,70 @@ export default function Payment({ const paymentVerb = mustBeGuaranteed ? guaranteeing : paying return ( - -
- {mustBeGuaranteed ? ( + <> + + + {mustBeGuaranteed ? ( +
+ + {intl.formatMessage({ + id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + })} + + +
+ ) : null} + {savedCreditCards?.length ? ( +
+ + {intl.formatMessage({ id: "MY SAVED CARDS" })} + +
+ {savedCreditCards?.map((savedCreditCard) => ( + + ))} +
+
+ ) : null}
- - {intl.formatMessage({ - id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", - })} - - -
- ) : null} - {savedCreditCards?.length ? ( -
- - {intl.formatMessage({ id: "MY SAVED CARDS" })} - + {savedCreditCards?.length ? ( + + {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} + + ) : null}
- {savedCreditCards?.map((savedCreditCard) => ( + + {availablePaymentOptions.map((paymentMethod) => ( ))}
- ) : null} -
- {savedCreditCards?.length ? ( - - {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} - - ) : null} -
- - {availablePaymentOptions.map((paymentMethod) => ( - - ))} -
-
-
- - - {intl.formatMessage({ - id: "I would like to get my booking confirmation via sms", - })} - - - - - +
{intl.formatMessage( { @@ -344,19 +372,48 @@ export default function Payment({ } )} - -
-
- -
- - + + + {intl.formatMessage({ + id: "I accept the terms and conditions", + })} + + + + + {intl.formatMessage({ + id: "I would like to get my booking confirmation via sms", + })} + + +
+
+ +
+ +
+ {priceChangeData ? ( + { + const allSearchParams = searchParams.size + ? `?${searchParams.toString()}` + : "" + router.push(`${selectRate(lang)}${allSearchParams}`) + }} + onAccept={() => priceChange.mutate({ confirmationNumber })} + /> + ) : null} + ) } diff --git a/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx b/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx new file mode 100644 index 000000000..c909c1232 --- /dev/null +++ b/components/HotelReservation/EnterDetails/PriceChangeDialog/index.tsx @@ -0,0 +1,73 @@ +import { Dialog, Modal, ModalOverlay } from "react-aria-components" +import { useIntl } from "react-intl" + +import { InfoCircleIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Title from "@/components/TempDesignSystem/Text/Title" + +import styles from "./priceChangeDialog.module.css" + +import { PriceChangeDialogProps } from "@/types/components/hotelReservation/enterDetails/priceChangeDialog" + +export default function PriceChangeDialog({ + isOpen, + oldPrice, + newPrice, + currency, + onCancel, + onAccept, +}: PriceChangeDialogProps) { + const intl = useIntl() + const title = intl.formatMessage({ id: "The price has increased" }) + + return ( + + + +
+
+ + + {title} + +
+ + {intl.formatMessage({ + id: "The price has increased since you selected your room.", + })} +
+ {intl.formatMessage({ + id: "You can still book the room but you need to confirm that you accept the new price", + })} +
+ + {intl.formatNumber(oldPrice, { style: "currency", currency })} + {" "} + + {intl.formatNumber(newPrice, { style: "currency", currency })} + + +
+
+ + +
+
+
+
+ ) +} diff --git a/components/HotelReservation/EnterDetails/PriceChangeDialog/priceChangeDialog.module.css b/components/HotelReservation/EnterDetails/PriceChangeDialog/priceChangeDialog.module.css new file mode 100644 index 000000000..b90f4c380 --- /dev/null +++ b/components/HotelReservation/EnterDetails/PriceChangeDialog/priceChangeDialog.module.css @@ -0,0 +1,85 @@ +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slide-up { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } +} + +.overlay { + align-items: center; + background: rgba(0, 0, 0, 0.5); + display: flex; + height: var(--visual-viewport-height); + justify-content: center; + left: 0; + position: fixed; + top: 0; + width: 100vw; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.modal { + &[data-entering] { + animation: slide-up 200ms; + } + &[data-exiting] { + animation: slide-up 200ms reverse ease-in-out; + } +} + +.dialog { + background-color: var(--Scandic-Brand-Pale-Peach); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: var(--Spacing-x5) var(--Spacing-x4); +} + +.header { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.titleContainer { + display: flex; + flex-direction: column; + align-items: center; +} + +.footer { + display: flex; + justify-content: center; + gap: var(--Spacing-x2); +} + +.oldPrice { + text-decoration: line-through; +} + +.newPrice { + font-size: 1.2em; +} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 11c558af9..7c03ccefb 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -25,6 +25,7 @@ width: 24px; height: 24px; min-width: 24px; + background: var(--UI-Input-Controls-Surface-Normal); border: 1px solid var(--UI-Input-Controls-Border-Normal); border-radius: 4px; transition: all 200ms; diff --git a/hooks/booking/useAvailablePaymentOptions.ts b/hooks/booking/useAvailablePaymentOptions.ts new file mode 100644 index 000000000..0947e3209 --- /dev/null +++ b/hooks/booking/useAvailablePaymentOptions.ts @@ -0,0 +1,23 @@ +"use client" + +import { useEffect, useState } from "react" + +import { PaymentMethodEnum } from "@/constants/booking" + +export function useAvailablePaymentOptions( + otherPaymentOptions: PaymentMethodEnum[] +) { + const [availablePaymentOptions, setAvailablePaymentOptions] = useState( + otherPaymentOptions.filter( + (option) => option !== PaymentMethodEnum.applePay + ) + ) + + useEffect(() => { + if (window.ApplePaySession) { + setAvailablePaymentOptions(otherPaymentOptions) + } + }, [otherPaymentOptions, setAvailablePaymentOptions]) + + return availablePaymentOptions +} diff --git a/hooks/booking/useHandleBookingStatus.ts b/hooks/booking/useHandleBookingStatus.ts index 7c1fafcca..b816d5797 100644 --- a/hooks/booking/useHandleBookingStatus.ts +++ b/hooks/booking/useHandleBookingStatus.ts @@ -10,18 +10,20 @@ export function useHandleBookingStatus({ expectedStatus, maxRetries, retryInterval, + enabled, }: { confirmationNumber: string | null expectedStatus: BookingStatusEnum maxRetries: number retryInterval: number + enabled: boolean }) { const retries = useRef(0) const query = trpc.booking.status.useQuery( { confirmationNumber: confirmationNumber ?? "" }, { - enabled: !!confirmationNumber, + enabled, refetchInterval: (query) => { retries.current = query.state.dataUpdateCount diff --git a/hooks/booking/usePaymentFailedToast.ts b/hooks/booking/usePaymentFailedToast.ts index cccbe2db0..961ab0c2b 100644 --- a/hooks/booking/usePaymentFailedToast.ts +++ b/hooks/booking/usePaymentFailedToast.ts @@ -43,6 +43,6 @@ export function usePaymentFailedToast() { const queryParams = new URLSearchParams(searchParams.toString()) queryParams.delete("errorCode") - router.replace(`${pathname}?${queryParams.toString()}`) - }, [searchParams, router, pathname, errorCode, errorMessage]) + router.push(`${pathname}?${queryParams.toString()}`) + }, [searchParams, pathname, errorCode, errorMessage, router]) } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 8a5d64ac4..92281e936 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -8,6 +8,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accept new price": "Accepter ny pris", "Accessibility": "Tilgængelighed", "Accessible Room": "Tilgængelighedsrum", "Activities": "Aktiviteter", @@ -162,6 +163,7 @@ "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", "Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!", + "I accept the terms and conditions": "Jeg accepterer vilkårene", "I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS", "Image gallery": "{name} - Billedgalleri", "In adults bed": "i de voksnes seng", @@ -358,6 +360,8 @@ "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.", "Terms and conditions": "Vilkår og betingelser", "Thank you": "Tak", + "The price has increased": "Prisen er steget", + "The price has increased since you selected your room.": "Prisen er steget, efter at du har valgt dit værelse.", "Theatre": "Teater", "There are no rooms available that match your request": "Der er ingen ledige værelser, der matcher din anmodning", "There are no rooms available that match your request.": "Der er ingen værelser tilgængelige, der matcher din forespørgsel.", @@ -407,6 +411,7 @@ "Yes, discard changes": "Ja, kasser ændringer", "Yes, remove my card": "Ja, fjern mit kort", "You can always change your mind later and add breakfast at the hotel.": "Du kan altid ombestemme dig senere og tilføje morgenmad på hotellet.", + "You can still book the room but you need to confirm that you accept the new price": "Du kan stadig booke værelset, men du skal bekræfte, at du accepterer den nye pris", "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", "You have # gifts waiting for you!": "Du har {amount} gaver, der venter på dig!", "You have no previous stays.": "Du har ingen tidligere ophold.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 3b5842619..c4f49ee93 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -8,6 +8,7 @@ "ALLG": "Allergie", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Über das Hotel", + "Accept new price": "Neuen Preis akzeptieren", "Accessibility": "Zugänglichkeit", "Accessible Room": "Barrierefreies Zimmer", "Activities": "Aktivitäten", @@ -162,6 +163,7 @@ "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", "Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!", + "I accept the terms and conditions": "Ich akzeptiere die Geschäftsbedingungen", "I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten", "Image gallery": "{name} - Bildergalerie", "In adults bed": "Im Bett der Eltern", @@ -358,6 +360,8 @@ "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.", "Terms and conditions": "Geschäftsbedingungen", "Thank you": "Danke", + "The price has increased": "Der Preis ist gestiegen", + "The price has increased since you selected your room.": "Der Preis ist gestiegen, nachdem Sie Ihr Zimmer ausgewählt haben.", "Theatre": "Theater", "There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", @@ -406,6 +410,7 @@ "Yes, discard changes": "Ja, Änderungen verwerfen", "Yes, remove my card": "Ja, meine Karte entfernen", "You can always change your mind later and add breakfast at the hotel.": "Sie können es sich später jederzeit anders überlegen und das Frühstück im Hotel hinzufügen.", + "You can still book the room but you need to confirm that you accept the new price": "Sie können das Zimmer noch buchen, aber Sie müssen bestätigen, dass Sie die neue Preis akzeptieren", "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", "You have # gifts waiting for you!": "Es warten {amount} Geschenke auf Sie!", "You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.", @@ -433,7 +438,7 @@ "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", "booking.selectRoom": "Vælg værelse", - "booking.terms": "Ved at betale med en af de tilgængelige betalingsmetoder, accepterer jeg vilkårene for denne booking og de generelle Vilkår og betingelser, og forstår, at Scandic vil behandle min personlige data i forbindelse med denne booking i henhold til Scandics Privatlivspolitik. Jeg accepterer, at Scandic kræver et gyldigt kreditkort under min besøg i tilfælde af, at noget er tilbagebetalt.", + "booking.terms": "Mit der Zahlung über eine der verfügbaren Zahlungsmethoden akzeptiere ich die Buchungsbedingungen und die allgemeinen Geschäftsbedingungen und verstehe, dass Scandic meine personenbezogenen Daten im Zusammenhang mit dieser Buchung gemäß der Scandic Datenschutzrichtlinie verarbeitet. Ich akzeptiere, dass Scandic während meines Aufenthalts eine gültige Kreditkarte für eventuelle Rückerstattungen benötigt.", "booking.thisRoomIsEquippedWith": "Dieses Zimmer ist ausgestattet mit", "breakfast.price": "{amount} {currency}/Nacht", "breakfast.price.free": "{amount} {currency} 0 {currency}/Nacht", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 42b4b9316..5b718083d 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -8,6 +8,7 @@ "ALLG": "Allergy", "About meetings & conferences": "About meetings & conferences", "About the hotel": "About the hotel", + "Accept new price": "Accept new price", "Accessibility": "Accessibility", "Accessible Room": "Accessibility room", "Activities": "Activities", @@ -174,6 +175,7 @@ "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", "Hurry up and use them before they expire!": "Hurry up and use them before they expire!", + "I accept the terms and conditions": "I accept the terms and conditions", "I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms", "Image gallery": "{name} - Image gallery", "In adults bed": "In adults bed", @@ -387,6 +389,8 @@ "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", + "The price has increased": "The price has increased", + "The price has increased since you selected your room.": "The price has increased since you selected your room.", "Theatre": "Theatre", "There are no rooms available that match your request.": "There are no rooms available that match your request.", "There are no transactions to display": "There are no transactions to display", @@ -437,6 +441,7 @@ "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", + "You can still book the room but you need to confirm that you accept the new price": "You can still book the room but you need to confirm that you accept the new price", "You canceled adding a new credit card.": "You canceled adding a new credit card.", "You have # gifts waiting for you!": "You have {amount} gifts waiting for you!", "You have no previous stays.": "You have no previous stays.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 94d09060c..ebc7470d7 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -8,6 +8,7 @@ "ALLG": "Allergia", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Tietoja hotellista", + "Accept new price": "Hyväksy uusi hinta", "Accessibility": "Saavutettavuus", "Accessible Room": "Esteetön huone", "Activities": "Aktiviteetit", @@ -162,6 +163,7 @@ "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", "Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!", + "I accept the terms and conditions": "Hyväksyn käyttöehdot", "I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä", "Image gallery": "{name} - Kuvagalleria", "In adults bed": "Aikuisten vuoteessa", @@ -359,6 +361,8 @@ "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.", "Terms and conditions": "Käyttöehdot", "Thank you": "Kiitos", + "The price has increased": "Hinta on noussut", + "The price has increased since you selected your room.": "Hinta on noussut, koska valitsit huoneen.", "Theatre": "Teatteri", "There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", @@ -407,6 +411,7 @@ "Yes, discard changes": "Kyllä, hylkää muutokset", "Yes, remove my card": "Kyllä, poista korttini", "You can always change your mind later and add breakfast at the hotel.": "Voit aina muuttaa mieltäsi myöhemmin ja lisätä aamiaisen hotelliin.", + "You can still book the room but you need to confirm that you accept the new price": "Voit vielä bookea huoneen, mutta sinun on vahvistettava, että hyväksyt uuden hinnan", "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", "You have # gifts waiting for you!": "Sinulla on {amount} lahjaa odottamassa sinua!", "You have no previous stays.": "Sinulla ei ole aiempia majoituksia.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index bffe5c2ac..b2501bfc7 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -8,6 +8,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accept new price": "Aksepterer ny pris", "Accessibility": "Tilgjengelighet", "Accessible Room": "Tilgjengelighetsrom", "Activities": "Aktiviteter", @@ -161,6 +162,7 @@ "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!", + "I accept the terms and conditions": "Jeg aksepterer vilkårene", "Image gallery": "{name} - Bildegalleri", "In adults bed": "i voksnes seng", "In crib": "i sprinkelseng", @@ -356,6 +358,8 @@ "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.", "Terms and conditions": "Vilkår og betingelser", "Thank you": "Takk", + "The price has increased": "Prisen er steget", + "The price has increased since you selected your room.": "Prisen er steget, etter at du har valgt rommet.", "Theatre": "Teater", "There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.", "There are no transactions to display": "Det er ingen transaksjoner å vise", @@ -404,6 +408,7 @@ "Yes, discard changes": "Ja, forkast endringer", "Yes, remove my card": "Ja, fjern kortet mitt", "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ombestemme deg senere og legge til frokost på hotellet.", + "You can still book the room but you need to confirm that you accept the new price": "Du kan fortsatt booke rommet, men du må bekrefte at du aksepterer den nye prisen", "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", "You have # gifts waiting for you!": "Du har {amount} gaver som venter på deg!", "You have no previous stays.": "Du har ingen tidligere opphold.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 883ee8e07..e2b16255e 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -8,6 +8,7 @@ "ALLG": "Allergi", "About meetings & conferences": "About meetings & conferences", "About the hotel": "Om hotellet", + "Accept new price": "Accepter ny pris", "Accessibility": "Tillgänglighet", "Accessible Room": "Tillgänglighetsrum", "Activities": "Aktiviteter", @@ -161,6 +162,7 @@ "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", "Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!", + "I accept the terms and conditions": "Jag accepterar villkoren", "Image gallery": "{name} - Bildgalleri", "In adults bed": "I vuxens säng", "In crib": "I spjälsäng", @@ -356,6 +358,8 @@ "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.", "Terms and conditions": "Allmänna villkor", "Thank you": "Tack", + "The price has increased": "Priset har ökat", + "The price has increased since you selected your room.": "Priset har ökat sedan du valde ditt rum.", "Theatre": "Teater", "There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.", "There are no transactions to display": "Det finns inga transaktioner att visa", @@ -404,6 +408,7 @@ "Yes, discard changes": "Ja, ignorera ändringar", "Yes, remove my card": "Ja, ta bort mitt kort", "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ändra dig senare och lägga till frukost på hotellet.", + "You can still book the room but you need to confirm that you accept the new price": "Du kan fortsatt boka rummet men du måste bekräfta att du accepterar det nya priset", "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", "You have # gifts waiting for you!": "Du har {amount} presenter som väntar på dig!", "You have no previous stays.": "Du har inga tidigare vistelser.", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 781a19f38..f64b83396 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -59,6 +59,9 @@ export namespace endpoints { export function status(confirmationNumber: string) { return `${bookings}/${confirmationNumber}/status` } + export function priceChange(confirmationNumber: string) { + return `${bookings}/${confirmationNumber}/priceChange` + } export const enum Stays { future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`, diff --git a/lib/api/index.ts b/lib/api/index.ts index 46bae9f88..5842b5597 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -34,13 +34,7 @@ export async function get( ) { const url = new URL(env.API_BASEURL) url.pathname = endpoint - const searchParams = new URLSearchParams(params) - if (searchParams.size) { - searchParams.forEach((value, key) => { - url.searchParams.append(key, value) - }) - url.searchParams.sort() - } + url.search = new URLSearchParams(params).toString() return wrappedFetch( url, merge.all([defaultOptions, { method: "GET" }, options]) @@ -55,13 +49,7 @@ export async function patch( const { body, ...requestOptions } = options const url = new URL(env.API_BASEURL) url.pathname = endpoint - const searchParams = new URLSearchParams(params) - if (searchParams.size) { - searchParams.forEach((value, key) => { - url.searchParams.set(key, value) - }) - url.searchParams.sort() - } + url.search = new URLSearchParams(params).toString() return wrappedFetch( url, merge.all([ @@ -80,13 +68,7 @@ export async function post( const { body, ...requestOptions } = options const url = new URL(env.API_BASEURL) url.pathname = endpoint - const searchParams = new URLSearchParams(params) - if (searchParams.size) { - searchParams.forEach((value, key) => { - url.searchParams.set(key, value) - }) - url.searchParams.sort() - } + url.search = new URLSearchParams(params).toString() return wrappedFetch( url, merge.all([ @@ -97,6 +79,25 @@ export async function post( ) } +export async function put( + endpoint: Endpoint | `${Endpoint}/${string}`, + options: RequestOptionsWithJSONBody, + params = {} +) { + const { body, ...requestOptions } = options + const url = new URL(env.API_BASEURL) + url.pathname = endpoint + url.search = new URLSearchParams(params).toString() + return wrappedFetch( + url, + merge.all([ + defaultOptions, + { body: JSON.stringify(body), method: "PUT" }, + requestOptions, + ]) + ) +} + export async function remove( endpoint: Endpoint | `${Endpoint}/${string}`, options: RequestOptionsWithOutBody, @@ -104,13 +105,7 @@ export async function remove( ) { const url = new URL(env.API_BASEURL) url.pathname = endpoint - const searchParams = new URLSearchParams(params) - if (searchParams.size) { - searchParams.forEach((value, key) => { - url.searchParams.set(key, value) - }) - url.searchParams.sort() - } + url.search = new URLSearchParams(params).toString() return wrappedFetch( url, merge.all([defaultOptions, { method: "DELETE" }, options]) diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index f838d201f..b6c82f906 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -83,6 +83,10 @@ export const createBookingInput = z.object({ payment: paymentSchema, }) +export const priceChangeInput = z.object({ + confirmationNumber: z.string(), +}) + // Query const confirmationNumberInput = z.object({ confirmationNumber: z.string(), diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts index dc3bad0fe..06ae675a7 100644 --- a/server/routers/booking/mutation.ts +++ b/server/routers/booking/mutation.ts @@ -6,7 +6,7 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc" import { getMembership } from "@/utils/user" -import { createBookingInput } from "./input" +import { createBookingInput, priceChangeInput } from "./input" import { createBookingSchema } from "./output" import type { Session } from "next-auth" @@ -20,6 +20,14 @@ const createBookingFailCounter = meter.createCounter( "trpc.bookings.create-fail" ) +const priceChangeCounter = meter.createCounter("trpc.bookings.price-change") +const priceChangeSuccessCounter = meter.createCounter( + "trpc.bookings.price-change-success" +) +const priceChangeFailCounter = meter.createCounter( + "trpc.bookings.price-change-fail" +) + async function getMembershipNumber( session: Session | null ): Promise { @@ -122,6 +130,71 @@ export const bookingMutationRouter = router({ query: loggingAttributes, }) ) + + return verifiedData.data + }), + priceChange: safeProtectedServiceProcedure + .input(priceChangeInput) + .mutation(async function ({ ctx, input }) { + const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken + const { confirmationNumber } = input + + priceChangeCounter.add(1, { confirmationNumber }) + + const headers = { + Authorization: `Bearer ${accessToken}`, + } + + const apiResponse = await api.put( + api.endpoints.v1.Booking.priceChange(confirmationNumber), + { + headers, + body: input, + } + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + priceChangeFailCounter.add(1, { + confirmationNumber, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + }), + }) + console.error( + "api.booking.priceChange error", + JSON.stringify({ + query: { confirmationNumber }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + priceChangeFailCounter.add(1, { + confirmationNumber, + error_type: "validation_error", + }) + console.error( + "api.booking.priceChange validation error", + JSON.stringify({ + query: { confirmationNumber }, + error: verifiedData.error, + }) + ) + return null + } + + priceChangeSuccessCounter.add(1, { confirmationNumber }) + return verifiedData.data }), }) diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 5c8879c00..997d7baaa 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -21,8 +21,8 @@ export const createBookingSchema = z errorMessage: z.string().nullable().optional(), priceChangedMetadata: z .object({ - roomPrice: z.number().nullable().optional(), - totalPrice: z.number().nullable().optional(), + roomPrice: z.number(), + totalPrice: z.number(), }) .nullable() .optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 9d3c71194..e6f5f43dc 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { ChildBedTypeEnum } from "@/constants/booking" +import { ChildBedTypeEnum, PaymentMethodEnum } from "@/constants/booking" import { dt } from "@/lib/dt" import { toLang } from "@/server/utils" @@ -376,6 +376,7 @@ const merchantInformationSchema = z.object({ return Object.entries(val) .filter(([_, enabled]) => enabled) .map(([key]) => key) + .filter((key): key is PaymentMethodEnum => !!key) }), }) diff --git a/types/components/hotelReservation/enterDetails/priceChangeDialog.ts b/types/components/hotelReservation/enterDetails/priceChangeDialog.ts new file mode 100644 index 000000000..32ae4996d --- /dev/null +++ b/types/components/hotelReservation/enterDetails/priceChangeDialog.ts @@ -0,0 +1,8 @@ +export type PriceChangeDialogProps = { + isOpen: boolean + oldPrice: number + newPrice: number + currency: string + onCancel: () => void + onAccept: () => void +} diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index eaea36f2e..eb8b6c299 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -1,3 +1,5 @@ +import { PaymentMethodEnum } from "@/constants/booking" + import { CreditCard, SafeUser } from "@/types/user" export interface SectionProps { @@ -30,7 +32,7 @@ export interface DetailsProps extends SectionProps {} export interface PaymentProps { user: SafeUser roomPrice: { publicPrice: number; memberPrice: number | undefined } - otherPaymentOptions: string[] + otherPaymentOptions: PaymentMethodEnum[] savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean } From 0865a553cdaa523e60ebc45ad5f687498bca6725 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 20 Nov 2024 17:54:57 +0100 Subject: [PATCH 23/58] fix(SW-360): set default validate behaviour to onBlur on signup page --- components/Forms/Signup/index.tsx | 8 ++++---- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + 6 files changed, 9 insertions(+), 4 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index 24fe2bd95..980b64212 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -37,6 +37,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { const email = intl.formatMessage({ id: "Email address" }) const phoneNumber = intl.formatMessage({ id: "Phone number" }) const zipCode = intl.formatMessage({ id: "Zip code" }) + const acceptTermsText = intl.formatMessage({ id: "signupPage.terms" }) const signupButtonText = intl.formatMessage({ id: "Sign up to Scandic Friends", }) @@ -66,8 +67,9 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { zipCode: "", }, password: "", + termsAccepted: false, }, - mode: "all", + mode: "onBlur", criteriaMode: "all", resolver: zodResolver(signUpSchema), reValidateMode: "onChange", @@ -157,9 +159,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { - {intl.formatMessage({ - id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", - })}{" "} + {acceptTermsText}{" "} Terms and Conditions. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic’s customer service", + "signupPage.terms": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index b2501bfc7..52b804b12 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -461,6 +461,7 @@ "points": "poeng", "room type": "romtype", "room types": "romtyper", + "signupPage.terms": "Ja, jeg godtar vilkårene og betingelsene for Scandic Friends og forstår at Scandic vil behandle mine personopplysninger i henhold til", "special character": "spesiell karakter", "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index e2b16255e..34b6935a2 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -462,6 +462,7 @@ "points": "poäng", "room type": "rumtyp", "room types": "rumstyper", + "signupPage.terms": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att behandla mina personuppgifter i enlighet med", "special character": "speciell karaktär", "spendable points expiring by": "{points} poäng förfaller {date}", "to": "till", From 6f980fe9cd8c4e89ba590491719a60bd36c9fb9f Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Thu, 21 Nov 2024 15:10:28 +0100 Subject: [PATCH 24/58] refactor(SW-360): review feedback --- components/Forms/Signup/index.tsx | 25 +++++++++++++++---------- i18n/dictionaries/da.json | 2 +- i18n/dictionaries/de.json | 2 +- i18n/dictionaries/en.json | 2 +- i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 2 +- i18n/dictionaries/sv.json | 2 +- server/routers/user/output.ts | 2 -- 8 files changed, 21 insertions(+), 17 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index 980b64212..b832a1d6e 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -37,7 +37,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { const email = intl.formatMessage({ id: "Email address" }) const phoneNumber = intl.formatMessage({ id: "Phone number" }) const zipCode = intl.formatMessage({ id: "Zip code" }) - const acceptTermsText = intl.formatMessage({ id: "signupPage.terms" }) const signupButtonText = intl.formatMessage({ id: "Sign up to Scandic Friends", }) @@ -159,15 +158,21 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { - {acceptTermsText}{" "} - - {intl.formatMessage({ id: "Scandic's Privacy Policy." })} - + {intl.formatMessage( + { id: "signupPage.terms" }, + { + termsLink: (str) => ( + + {str} + + ), + } + )} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 86b34a97c..87f15501a 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -465,7 +465,7 @@ "points": "Point", "room type": "værelsestype", "room types": "værelsestyper", - "signupPage.terms": "Ja, jeg accepterer vilkårene og betingelserne for Scandic Friends og forstår, at Scandic vil behandle mine personlige data i overensstemmelse med", + "signupPage.terms": "Ja, jeg accepterer vilkårene og betingelserne for Scandic Friends og forstår, at Scandic vil behandle mine personlige data i overensstemmelse med Scandic's integritetspolicy.", "special character": "speciel karakter", "spendable points expiring by": "{points} Brugbare point udløber den {date}", "to": "til", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index af1a98f3c..7a3c679d2 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -464,7 +464,7 @@ "points": "Punkte", "room type": "zimmerart", "room types": "zimmerarten", - "signupPage.terms": "Ja, ich akzeptiere die Allgemeinen Geschäftsbedingungen für Scandic Friends und verstehe, dass Scandic meine persönlichen Daten gemäß", + "signupPage.terms": "Ja, ich akzeptiere die Allgemeinen Geschäftsbedingungen für Scandic Friends und verstehe, dass Scandic meine persönlichen Daten gemäß Scandics Datenschutzrichtlinie.", "special character": "sonderzeichen", "spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}", "to": "zu", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 959f53dde..7b7973fa2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -504,7 +504,7 @@ "room type": "room type", "room types": "room types", "signup.terms": "By signing up you accept the Scandic Friends Terms and Conditions. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic’s customer service", - "signupPage.terms": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", + "signupPage.terms": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with Scandic's Privacy Policy.", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index ebc7470d7..09b8df7a3 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -463,6 +463,7 @@ "points": "pistettä", "room type": "huonetyyppi", "room types": "huonetyypit", + "signupPage.terms": "Kyllä, hyväksyn Scandic Friends -käyttöehdot ja ymmärrän, että Scandic käsittelee henkilötietojani Scandicin tietosuojakäytännön mukaisesti.", "special character": "erikoishahmo", "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 52b804b12..8ae54a9e1 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -461,7 +461,7 @@ "points": "poeng", "room type": "romtype", "room types": "romtyper", - "signupPage.terms": "Ja, jeg godtar vilkårene og betingelsene for Scandic Friends og forstår at Scandic vil behandle mine personopplysninger i henhold til", + "signupPage.terms": "Ja, jeg godtar vilkårene og betingelsene for Scandic Friends og forstår at Scandic vil behandle mine personopplysninger i henhold til Scandics integritetspolicy.", "special character": "spesiell karakter", "spendable points expiring by": "{points} Brukbare poeng utløper innen {date}", "to": "til", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 34b6935a2..de3857f1a 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -462,7 +462,7 @@ "points": "poäng", "room type": "rumtyp", "room types": "rumstyper", - "signupPage.terms": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att behandla mina personuppgifter i enlighet med", + "signupPage.terms": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att behandla mina personuppgifter i enlighet med Scandics integritetspolicy.", "special character": "speciell karaktär", "spendable points expiring by": "{points} poäng förfaller {date}", "to": "till", diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index 616419047..a9e7a0416 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -1,8 +1,6 @@ import { z } from "zod" import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries" -import { passwordValidator } from "@/utils/passwordValidator" -import { phoneValidator } from "@/utils/phoneValidator" import { getMembership } from "@/utils/user" export const membershipSchema = z.object({ From 0d972367dfeb3cee9fd03a398443f1072483f745 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 22 Nov 2024 16:01:47 +0100 Subject: [PATCH 25/58] fix(SW-360): remove container queries to fix stacking context bug affecting dropdowns in signup --- components/Forms/Signup/index.tsx | 3 +- .../Form/Date/date.module.css | 19 +-- .../TempDesignSystem/Form/Phone/index.tsx | 120 +++++++++--------- .../Form/Phone/phone.module.css | 19 +-- 4 files changed, 75 insertions(+), 86 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index b832a1d6e..9d89456fb 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -68,10 +68,11 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { password: "", termsAccepted: false, }, - mode: "onBlur", + mode: "all", criteriaMode: "all", resolver: zodResolver(signUpSchema), reValidateMode: "onChange", + shouldFocusError: true, }) async function onSubmit(data: SignUpSchema) { diff --git a/components/TempDesignSystem/Form/Date/date.module.css b/components/TempDesignSystem/Form/Date/date.module.css index 7f492a293..ecbdd3ee2 100644 --- a/components/TempDesignSystem/Form/Date/date.module.css +++ b/components/TempDesignSystem/Form/Date/date.module.css @@ -1,8 +1,3 @@ -/* Leaving, will most likely get deleted */ -.datePicker { - container-name: datePickerContainer; - container-type: inline-size; -} .container { display: grid; gap: var(--Spacing-x2); @@ -11,6 +6,13 @@ width: var(--width); } +@media (max-width: 350px) { + .container { + display: flex; + flex-direction: column; + } +} + .day { grid-area: day; } @@ -31,10 +33,3 @@ .year.invalid > div > div { border-color: var(--Scandic-Red-60); } - -@container datePickerContainer (max-width: 350px) { - .container { - display: flex; - flex-direction: column; - } -} diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index 5ff8f3482..9c413fc0a 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -78,69 +78,67 @@ export default function Phone({ } return ( -
-
- ( - - )} - /> - + + + + )} + /> + + - - - -
+ value={inputValue} + /> + +
) } diff --git a/components/TempDesignSystem/Form/Phone/phone.module.css b/components/TempDesignSystem/Form/Phone/phone.module.css index 31de5be30..2869cb3c0 100644 --- a/components/TempDesignSystem/Form/Phone/phone.module.css +++ b/components/TempDesignSystem/Form/Phone/phone.module.css @@ -1,11 +1,7 @@ -.wrapper { - container-name: phoneContainer; - container-type: inline-size; -} .phone { display: grid; + grid-template-columns: 1fr; gap: var(--Spacing-x2); - grid-template-columns: minmax(124px, 164px) 1fr; --react-international-phone-background-color: var(--Main-Grey-White); --react-international-phone-border-color: var(--Scandic-Beige-40); @@ -28,6 +24,12 @@ ); } +@media (min-width: 385px) { + .phone { + grid-template-columns: minmax(124px, 164px) 1fr; + } +} + .phone:has(.input:active, .input:focus) { --react-international-phone-border-color: var(--Scandic-Blue-90); } @@ -104,10 +106,3 @@ justify-self: flex-start; padding: 0; } - -@container phoneContainer (max-width: 350px) { - .phone { - display: flex; - flex-direction: column; - } -} From 03db7cec16c03237c5c04ddd3bef5fb1482d8315 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 25 Nov 2024 09:32:16 +0100 Subject: [PATCH 26/58] chore(SW-360): remove unused translation --- i18n/dictionaries/da.json | 1 - i18n/dictionaries/de.json | 1 - i18n/dictionaries/en.json | 1 - i18n/dictionaries/fi.json | 1 - i18n/dictionaries/no.json | 1 - i18n/dictionaries/sv.json | 1 - 6 files changed, 6 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 87f15501a..6fa96b8f1 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -312,7 +312,6 @@ "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "Scandic's Privacy Policy.": "Scandic's integritetspolicy.", "Search": "Søge", "See all FAQ": "Se alle FAQ", "See all photos": "Se alle billeder", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 7a3c679d2..433954331 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -312,7 +312,6 @@ "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "Scandic's Privacy Policy.": "Scandics Datenschutzrichtlinie.", "Search": "Suchen", "See all FAQ": "Siehe alle FAQ", "See all photos": "Alle Fotos ansehen", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 7b7973fa2..bbb4e4770 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -340,7 +340,6 @@ "Save card to profile": "Save card to profile", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "Scandic's Privacy Policy.": "Scandic's Privacy Policy.", "Search": "Search", "See all FAQ": "See all FAQ", "See all photos": "See all photos", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 09b8df7a3..007d0b968 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -313,7 +313,6 @@ "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "Scandic's Privacy Policy.": "Scandicin tietosuojavalmioksi.", "Search": "Haku", "See all FAQ": "Katso kaikki UKK", "See all photos": "Katso kaikki kuvat", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 8ae54a9e1..7b5a568a6 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -310,7 +310,6 @@ "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "Scandic's Privacy Policy.": "Scandics integritetspolicy.", "Search": "Søk", "See all FAQ": "Se alle FAQ", "See all photos": "Se alle bilder", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index de3857f1a..9bf840a18 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -310,7 +310,6 @@ "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", - "Scandic's Privacy Policy.": "Scandics integritetspolicy.", "Search": "Sök", "See all FAQ": "Se alla FAQ", "See all photos": "Se alla foton", From 79f48adbf59024e976b8030088d8e6f93b9bf650 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 25 Nov 2024 10:26:18 +0100 Subject: [PATCH 27/58] fix(SW-360): scroll to first validation + remove unused translations --- components/Forms/Signup/index.tsx | 13 ++++++++++++- i18n/dictionaries/da.json | 1 - i18n/dictionaries/de.json | 1 - i18n/dictionaries/fi.json | 1 - i18n/dictionaries/no.json | 1 - i18n/dictionaries/sv.json | 1 - 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index 9d89456fb..a8d05d335 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -79,6 +79,17 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { signup.mutate({ ...data, language: lang }) } + async function handleValidation() { + const result = await methods.trigger() + if (!result) { + // Get first error field and focus on it. + const firstError = Object.keys(methods.formState.errors)[0] + if (firstError) { + methods.setFocus(firstError as keyof SignUpSchema) + } + } + } + return (
{title} @@ -190,7 +201,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { type="button" theme="base" intent="primary" - onClick={() => methods.trigger()} + onClick={handleValidation} data-testid="trigger-validation" > {signupButtonText} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 6fa96b8f1..3acc7ad46 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -406,7 +406,6 @@ "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", "Year": "År", "Yes": "Ja", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg accepterer vilkårene for Scandic Friends og forstår, at Scandic vil behandle mine personlige oplysninger i henhold til", "Yes, discard changes": "Ja, kasser ændringer", "Yes, remove my card": "Ja, fjern mit kort", "You can always change your mind later and add breakfast at the hotel.": "Du kan altid ombestemme dig senere og tilføje morgenmad på hotellet.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 433954331..8c4a26459 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -405,7 +405,6 @@ "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", "Year": "Jahr", "Yes": "Ja", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, ich akzeptiere die Geschäftsbedingungen für Scandic Friends und erkenne an, dass Scandic meine persönlichen Daten in Übereinstimmung mit", "Yes, discard changes": "Ja, Änderungen verwerfen", "Yes, remove my card": "Ja, meine Karte entfernen", "You can always change your mind later and add breakfast at the hotel.": "Sie können es sich später jederzeit anders überlegen und das Frühstück im Hotel hinzufügen.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 007d0b968..4f6965532 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -406,7 +406,6 @@ "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", "Year": "Vuosi", "Yes": "Kyllä", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Kyllä, hyväksyn Scandic Friends -käyttöehdot ja ymmärrän, että Scandic käsittelee minun henkilötietoni asianmukaisesti", "Yes, discard changes": "Kyllä, hylkää muutokset", "Yes, remove my card": "Kyllä, poista korttini", "You can always change your mind later and add breakfast at the hotel.": "Voit aina muuttaa mieltäsi myöhemmin ja lisätä aamiaisen hotelliin.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 7b5a568a6..5494b85ef 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -403,7 +403,6 @@ "Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Year": "År", "Yes": "Ja", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg aksepterer vilkårene for Scandic Friends og forstår at Scandic vil behandle mine personlige opplysninger i henhold til", "Yes, discard changes": "Ja, forkast endringer", "Yes, remove my card": "Ja, fjern kortet mitt", "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ombestemme deg senere og legge til frokost på hotellet.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 9bf840a18..e651b14d6 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -403,7 +403,6 @@ "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", "Year": "År", "Yes": "Ja", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att bearbeta mina personliga uppgifter i enlighet med", "Yes, discard changes": "Ja, ignorera ändringar", "Yes, remove my card": "Ja, ta bort mitt kort", "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ändra dig senare och lägga till frukost på hotellet.", From 3967ec604038e26419aea285497aba16d968c71d Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 25 Nov 2024 10:28:50 +0100 Subject: [PATCH 28/58] chore(SW-360): proper formatting of AriaInputWithLabel --- .../TempDesignSystem/Form/Input/AriaInputWithLabel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/TempDesignSystem/Form/Input/AriaInputWithLabel/index.tsx b/components/TempDesignSystem/Form/Input/AriaInputWithLabel/index.tsx index 05402f093..0f375cf1f 100644 --- a/components/TempDesignSystem/Form/Input/AriaInputWithLabel/index.tsx +++ b/components/TempDesignSystem/Form/Input/AriaInputWithLabel/index.tsx @@ -1,4 +1,4 @@ -import { type ForwardedRef,forwardRef } from "react" +import { type ForwardedRef, forwardRef } from "react" import { Input as AriaInput, Label as AriaLabel } from "react-aria-components" import Label from "@/components/TempDesignSystem/Form/Label" From c8eaae792852388a7f126db06cd5c11075a61026 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Mon, 25 Nov 2024 11:15:59 +0100 Subject: [PATCH 29/58] fix(SW-360): change button to signup to trigger shouldFocusError --- components/Forms/Signup/index.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index a8d05d335..11f59c2aa 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -79,17 +79,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { signup.mutate({ ...data, language: lang }) } - async function handleValidation() { - const result = await methods.trigger() - if (!result) { - // Get first error field and focus on it. - const firstError = Object.keys(methods.formState.errors)[0] - if (firstError) { - methods.setFocus(firstError as keyof SignUpSchema) - } - } - } - return (
{title} @@ -198,10 +187,10 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { {!methods.formState.isValid ? (
- {field.value ? ( -
- {Object.entries(passwordValidators).map( - ([key, { message }]) => ( - - - - {getErrorMessage(key as PasswordValidatorKey)} - - - ) - )} -
- ) : null} + + + {!field.value && fieldState.error ? ( @@ -134,3 +109,43 @@ function Icon({ errorMessage, errors }: IconProps) { ) } + +function PasswordValidation({ + value, + errors, +}: { + value: string + errors: string[] +}) { + const intl = useIntl() + + if (!value) return null + + function getErrorMessage(key: PasswordValidatorKey) { + switch (key) { + case "length": + return `10 ${intl.formatMessage({ id: "to" })} 40 ${intl.formatMessage({ id: "characters" })}` + case "hasUppercase": + return `1 ${intl.formatMessage({ id: "uppercase letter" })}` + case "hasLowercase": + return `1 ${intl.formatMessage({ id: "lowercase letter" })}` + case "hasNumber": + return `1 ${intl.formatMessage({ id: "number" })}` + case "hasSpecialChar": + return `1 ${intl.formatMessage({ id: "special character" })}` + } + } + + return ( +
+ {Object.entries(passwordValidators).map(([key, { message }]) => ( + + + + {getErrorMessage(key as PasswordValidatorKey)} + + + ))} +
+ ) +} From 4311def1820e115346c2da5f59910b1ba914fe5d Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Tue, 26 Nov 2024 11:57:40 +0100 Subject: [PATCH 31/58] fix(SW-360): remove placeholder prop to fix ios select input bug --- components/Forms/Signup/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index 11f59c2aa..e314b6ab6 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -147,7 +147,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { From b3ab0613f6dea74848a0752f73deb5fa7bfb4eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Mon, 25 Nov 2024 10:33:11 +0100 Subject: [PATCH 32/58] fix: make headerRow optional --- server/routers/contentstack/schemas/blocks/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/contentstack/schemas/blocks/table.ts b/server/routers/contentstack/schemas/blocks/table.ts index 905c20603..0083a0c25 100644 --- a/server/routers/contentstack/schemas/blocks/table.ts +++ b/server/routers/contentstack/schemas/blocks/table.ts @@ -25,7 +25,7 @@ export const tableSchema = z.object({ data: z.array(z.object({}).catchall(z.string())), skipReset: z.boolean(), tableActionEnabled: z.boolean(), - headerRowAdded: z.boolean(), + headerRowAdded: z.boolean().optional().default(false), }), }), }) From 257900b7a8e321038ee8b4ce7272a3d4432b5ffa Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Fri, 22 Nov 2024 16:24:51 +0100 Subject: [PATCH 33/58] fix(SW-993): toast is now full width on mobile updated sonner to latest version increased default duration of toast to 5 sec --- components/TempDesignSystem/Toasts/index.tsx | 2 +- .../TempDesignSystem/Toasts/toasts.module.css | 7 ++++++- package-lock.json | 13 ++++++------- package.json | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/components/TempDesignSystem/Toasts/index.tsx b/components/TempDesignSystem/Toasts/index.tsx index f78360a0b..407eef5a1 100644 --- a/components/TempDesignSystem/Toasts/index.tsx +++ b/components/TempDesignSystem/Toasts/index.tsx @@ -16,7 +16,7 @@ import { toastVariants } from "./variants" import styles from "./toasts.module.css" export function ToastHandler() { - return + return } function getIcon(variant: ToastsProps["variant"]) { diff --git a/components/TempDesignSystem/Toasts/toasts.module.css b/components/TempDesignSystem/Toasts/toasts.module.css index 5a9fc2ef5..d49b8d57e 100644 --- a/components/TempDesignSystem/Toasts/toasts.module.css +++ b/components/TempDesignSystem/Toasts/toasts.module.css @@ -6,7 +6,12 @@ background: var(--Base-Surface-Primary-light-Normal); box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, 0.08); align-items: center; - width: var(--width); +} + +@media screen and (min-width: 768px) { + .toast { + width: var(--width); + } } .toast .message { diff --git a/package-lock.json b/package-lock.json index b645f1f83..202dbf8ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "react-international-phone": "^4.2.6", "react-intl": "^6.6.8", "server-only": "^0.0.1", - "sonner": "^1.5.0", + "sonner": "^1.7.0", "superjson": "^2.2.1", "usehooks-ts": "3.1.0", "zod": "^3.22.4", @@ -18205,13 +18205,12 @@ } }, "node_modules/sonner": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", - "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", - "license": "MIT", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.0.tgz", + "integrity": "sha512-W6dH7m5MujEPyug3lpI2l3TC3Pp1+LTgK0Efg+IHDrBbtEjyCmCHHo6yfNBOsf1tFZ6zf+jceWwB38baC8yO9g==", "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/sort-object-keys": { diff --git a/package.json b/package.json index 38301e51a..e3dd19c1b 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "react-international-phone": "^4.2.6", "react-intl": "^6.6.8", "server-only": "^0.0.1", - "sonner": "^1.5.0", + "sonner": "^1.7.0", "superjson": "^2.2.1", "usehooks-ts": "3.1.0", "zod": "^3.22.4", From 75c811eb329ab50b7aa165e2a19141095cd5666f Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 18 Nov 2024 10:31:32 +0100 Subject: [PATCH 34/58] feat(SW-201): Added hotel metadata --- i18n/dictionaries/en.json | 5 +- lib/graphql/Query/HotelPage/Metadata.graphql | 19 ++++++++ .../routers/contentstack/metadata/output.ts | 20 ++++++-- server/routers/contentstack/metadata/query.ts | 42 ++++++++++++++-- server/routers/contentstack/metadata/utils.ts | 48 ++++++++++++------- 5 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 lib/graphql/Query/HotelPage/Metadata.graphql diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index bbb4e4770..9f14b7f25 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -509,6 +509,7 @@ "to": "to", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", - "{amount} {currency}": "{amount} {currency}", - "{card} ending with {cardno}": "{card} ending with {cardno}" + "{card} ending with {cardno}": "{card} ending with {cardno}", + "{difference}{amount} {currency}": "{difference}{amount} {currency}", + "Stay at HOTEL_NAME | Hotel in DESTINATION": "Stay at {hotelName} | Hotel in {destination}" } diff --git a/lib/graphql/Query/HotelPage/Metadata.graphql b/lib/graphql/Query/HotelPage/Metadata.graphql new file mode 100644 index 000000000..6daed5c86 --- /dev/null +++ b/lib/graphql/Query/HotelPage/Metadata.graphql @@ -0,0 +1,19 @@ +#import "../../Fragments/Metadata.graphql" +#import "../../Fragments/System.graphql" + +query GetHotelPageMetadata($locale: String!, $uid: String!) { + hotel_page(locale: $locale, uid: $uid) { + hotel_page_id + web { + breadcrumbs { + title + } + seo_metadata { + ...Metadata + } + } + system { + ...System + } + } +} diff --git a/server/routers/contentstack/metadata/output.ts b/server/routers/contentstack/metadata/output.ts index 9474dd3fe..d9850dc97 100644 --- a/server/routers/contentstack/metadata/output.ts +++ b/server/routers/contentstack/metadata/output.ts @@ -1,7 +1,7 @@ import { z } from "zod" import { tempImageVaultAssetSchema } from "../schemas/imageVault" -import { getDescription, getImages, getTitle } from "./utils" +import { getDescription, getImage, getTitle } from "./utils" import type { Metadata } from "next" @@ -71,16 +71,26 @@ export const rawMetadataSchema = z.object({ .nullable(), hero_image: tempImageVaultAssetSchema.nullable(), blocks: metaDataBlocksSchema, + hotel_page_id: z.string().optional().nullable(), + hotelData: z + .object({ + name: z.string(), + city: z.string(), + description: z.string(), + image: z.object({ url: z.string(), alt: z.string() }).nullable(), + }) + .optional() + .nullable(), }) -export const metadataSchema = rawMetadataSchema.transform((data) => { +export const metadataSchema = rawMetadataSchema.transform(async (data) => { const noIndex = !!data.web?.seo_metadata?.noindex const metadata: Metadata = { - title: getTitle(data), - description: getDescription(data), + title: await getTitle(data), + description: await getDescription(data), openGraph: { - images: getImages(data), + images: getImage(data), }, } diff --git a/server/routers/contentstack/metadata/query.ts b/server/routers/contentstack/metadata/query.ts index df239d44a..4e5d78728 100644 --- a/server/routers/contentstack/metadata/query.ts +++ b/server/routers/contentstack/metadata/query.ts @@ -4,13 +4,15 @@ import { cache } from "react" import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql" import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql" import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql" +import { GetHotelPageMetadata } from "@/lib/graphql/Query/HotelPage/Metadata.graphql" import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql" import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" -import { contentstackExtendedProcedureUID, router } from "@/server/trpc" +import { contentStackUidWithServiceProcedure, router } from "@/server/trpc" import { generateTag } from "@/utils/generateTag" +import { getHotelData } from "../../hotels/query" import { metadataSchema } from "./output" import { affix } from "./utils" @@ -86,10 +88,10 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata( return response.data }) -function getTransformedMetadata(data: unknown) { +async function getTransformedMetadata(data: unknown) { transformMetadataCounter.add(1) console.info("contentstack.metadata transform start") - const validatedMetadata = metadataSchema.safeParse(data) + const validatedMetadata = await metadataSchema.safeParseAsync(data) if (!validatedMetadata.success) { transformMetadataFailCounter.add(1, { @@ -112,7 +114,7 @@ function getTransformedMetadata(data: unknown) { } export const metadataQueryRouter = router({ - get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { + get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => { const variables = { lang: ctx.lang, uid: ctx.uid, @@ -139,6 +141,38 @@ export const metadataQueryRouter = router({ loyalty_page: RawMetadataSchema }>(GetLoyaltyPageMetadata, variables) return getTransformedMetadata(loyaltyPageResponse.loyalty_page) + case PageTypeEnum.hotelPage: + const hotelPageResponse = await fetchMetadata<{ + hotel_page: RawMetadataSchema + }>(GetHotelPageMetadata, variables) + const hotelPageData = hotelPageResponse.hotel_page + const hotelData = hotelPageData.hotel_page_id + ? await getHotelData( + { hotelId: hotelPageData.hotel_page_id, language: ctx.lang }, + ctx.serviceToken + ) + : null + + const rawHotelData = hotelPageData + + if (hotelData?.data.attributes) { + const attributes = hotelData.data.attributes + const images = attributes.gallery?.smallerImages + + rawHotelData.hotelData = { + name: attributes.name, + city: attributes.cityName, + description: attributes.hotelContent.texts.descriptions.short, + image: images?.length + ? { + url: images[0].imageSizes.small, + alt: images[0].metaData.altText, + } + : null, + } + } + + return getTransformedMetadata(rawHotelData) default: return null } diff --git a/server/routers/contentstack/metadata/utils.ts b/server/routers/contentstack/metadata/utils.ts index 3cd9abdd0..0cdb0a886 100644 --- a/server/routers/contentstack/metadata/utils.ts +++ b/server/routers/contentstack/metadata/utils.ts @@ -1,3 +1,5 @@ +import { getIntl } from "@/i18n" + import { RTETypeEnum } from "@/types/rte/enums" import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata" @@ -58,11 +60,18 @@ function truncateTextAfterLastPeriod( return `${maxLengthText}...` } -export function getTitle(data: RawMetadataSchema) { +export async function getTitle(data: RawMetadataSchema) { + const intl = await getIntl() const metadata = data.web?.seo_metadata if (metadata?.title) { return metadata.title } + if (data.hotelData) { + return intl.formatMessage( + { id: "Stay at HOTEL_NAME | Hotel in DESTINATION" }, + { hotelName: data.hotelData.name, destination: data.hotelData.city } + ) + } if (data.web?.breadcrumbs?.title) { return data.web.breadcrumbs.title } @@ -75,11 +84,15 @@ export function getTitle(data: RawMetadataSchema) { return "" } -export function getDescription(data: RawMetadataSchema) { +export async function getDescription(data: RawMetadataSchema) { + const intl = await getIntl() const metadata = data.web?.seo_metadata if (metadata?.description) { return metadata.description } + if (data.hotelData) { + return data.hotelData.description + } if (data.preamble) { return truncateTextAfterLastPeriod(data.preamble) } @@ -108,22 +121,23 @@ export function getImages(data: RawMetadataSchema) { // Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15) if (metadataImage) { - return [ - { - url: metadataImage.url, - width: metadataImage.dimensions.width, - height: metadataImage.dimensions.height, - }, - ] + return { + url: metadataImage.url, + alt: metadataImage.meta.alt || undefined, + width: metadataImage.dimensions.width, + height: metadataImage.dimensions.height, + } + } + if (data.hotelData?.image) { + return data.hotelData.image } if (heroImage) { - return [ - { - url: heroImage.url, - width: heroImage.dimensions.width, - height: heroImage.dimensions.height, - }, - ] + return { + url: heroImage.url, + alt: heroImage.meta.alt || undefined, + width: heroImage.dimensions.width, + height: heroImage.dimensions.height, + } } - return [] + return undefined } From ca2f60253ff6e97e975d1fcd19481a4d84bb9aeb Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 18 Nov 2024 14:55:24 +0100 Subject: [PATCH 35/58] feat(SW-201): Refactoring how we fetch hotel page data --- .../(public)/[contentType]/[uid]/page.tsx | 10 +- components/ContentType/HotelPage/index.tsx | 129 +++++++++------ lib/trpc/memoizedRequests/index.ts | 4 + server/routers/contentstack/metadata/utils.ts | 2 +- server/routers/hotels/query.ts | 149 +----------------- types/components/hotelPage/facilities.ts | 2 +- types/components/hotelPage/hotelPage.ts | 3 + 7 files changed, 99 insertions(+), 200 deletions(-) create mode 100644 types/components/hotelPage/hotelPage.ts diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 215e3aec8..f81aa2edf 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { isSignupPage } from "@/constants/routes/signup" import { env } from "@/env/server" +import { getHotelPage } from "@/lib/trpc/memoizedRequests" import HotelPage from "@/components/ContentType/HotelPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage" @@ -19,7 +20,7 @@ import { export { generateMetadata } from "@/utils/generateMetadata" -export default function ContentTypePage({ +export default async function ContentTypePage({ params, }: PageArgs) { setLang(params.lang) @@ -57,7 +58,12 @@ export default function ContentTypePage({ if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() } - return + const hotelPageData = await getHotelPage() + return hotelPageData ? ( + + ) : ( + notFound() + ) default: const type: never = params.contentType console.error(`Unsupported content type given: ${type}`) diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 3e4893065..f5acca331 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,6 +1,8 @@ +import { notFound } from "next/navigation" + import hotelPageParams from "@/constants/routes/hotelPageParams" import { env } from "@/env/server" -import { serverClient } from "@/lib/trpc/server" +import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests" import AccordionSection from "@/components/Blocks/Accordion" import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek" @@ -16,65 +18,96 @@ import MapCard from "./Map/MapCard" import MapWithCardWrapper from "./Map/MapWithCard" import MobileMapToggle from "./Map/MobileMapToggle" import StaticMap from "./Map/StaticMap" +import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise" import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" import IntroSection from "./IntroSection" import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" -import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks" +import { AboutTheHotelSidePeek } from "./SidePeeks" import TabNavigation from "./TabNavigation" import styles from "./hotelPage.module.css" +import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" +import { HotelPageProps } from "@/types/components/hotelPage/hotelPage" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" +import { Facility } from "@/types/hotel" -export default async function HotelPage() { - const intl = await getIntl() - const lang = getLang() +export default async function HotelPage({ hotelId }: HotelPageProps) { const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID - const hotelData = await serverClient().hotel.get() - if (!hotelData) { - return null + const lang = getLang() + const [intl, hotelPageData, hotelData] = await Promise.all([ + getIntl(), + getHotelPage(), + getHotelData({ hotelId, language: lang }), + ]) + + if (!hotelData?.data || !hotelPageData) { + return notFound() } + const { faq, content } = hotelPageData const { - hotelId, - hotelName, - hotelDescriptions, - hotelLocation, - hotelAddress, - hotelRatings, - hotelDetailedFacilities, - hotelImages, - roomCategories, - activitiesCard, + name, + address, pointsOfInterest, - facilities, - faq, - alerts, + gallery, + specialAlerts, + healthAndWellness, + restaurantImages, + conferencesAndMeetings, + hotelContent, + detailedFacilities, healthFacilities, - contact, - socials, - ecoLabels, - } = hotelData + contactInformation, + socialMedia, + hotelFacts, + location, + ratings, + } = hotelData.data.attributes + const roomCategories = + hotelData.included?.filter((item) => item.type === "roomcategories") || [] + const images = gallery?.smallerImages + const description = hotelContent.texts.descriptions.short + const activitiesCard = content?.[0]?.upcoming_activities_card || null + + const facilities: Facility[] = [ + { + ...restaurantImages, + id: FacilityCardTypeEnum.restaurant, + headingText: restaurantImages?.headingText ?? "", + heroImages: restaurantImages?.heroImages ?? [], + }, + { + ...conferencesAndMeetings, + id: FacilityCardTypeEnum.conference, + headingText: conferencesAndMeetings?.headingText ?? "", + heroImages: conferencesAndMeetings?.heroImages ?? [], + }, + { + ...healthAndWellness, + id: FacilityCardTypeEnum.wellness, + headingText: healthAndWellness?.headingText ?? "", + heroImages: healthAndWellness?.heroImages ?? [], + }, + ] const topThreePois = pointsOfInterest.slice(0, 3) const coordinates = { - lat: hotelLocation.latitude, - lng: hotelLocation.longitude, + lat: location.latitude, + lng: location.longitude, } return (
- {hotelImages?.length && ( - - )} + {images?.length && }
@@ -82,18 +115,18 @@ export default async function HotelPage() {
- +
- {alerts.length ? ( + {specialAlerts.length ? (
- {alerts.map((alert) => ( + {specialAlerts.map((alert) => ( ( - GetHotelPage, - { - locale: lang, - uid, - }, - { - cache: "force-cache", - next: { - tags, - }, - } - ) - - if (!response.data) { - throw notFound(response) - } - - const hotelPageData = hotelPageSchema.safeParse(response.data) - if (!hotelPageData.success) { - console.error( - `Failed to validate Hotel Page - (uid: ${uid}, lang: ${lang})` - ) - console.error(hotelPageData.error) - return null - } - - return hotelPageData.data.hotel_page -} - export const getHotelData = cache( async (input: HotelDataInput, serviceToken: string) => { const { hotelId, language, isCardOnlyPayment } = input @@ -273,90 +210,6 @@ export const getHotelData = cache( ) export const hotelQueryRouter = router({ - get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => { - const { lang, uid } = ctx - - const contentstackData = await getContentstackData(lang, uid) - const hotelId = contentstackData?.hotel_page_id - - if (!hotelId) { - throw notFound(`Hotel not found for uid: ${uid}`) - } - - const hotelData = await getHotelData( - { - hotelId, - language: ctx.lang, - }, - ctx.serviceToken - ) - - if (!hotelData) { - throw notFound() - } - - const included = hotelData.included || [] - - const hotelAttributes = hotelData.data.attributes - const images = hotelAttributes.gallery?.smallerImages - const hotelAlerts = hotelAttributes.specialAlerts - - const roomCategories = included - ? included.filter((item) => item.type === "roomcategories") - : [] - - const activities = contentstackData?.content - ? contentstackData?.content[0] - : null - - const facilities: Facility[] = [ - { - ...hotelData.data.attributes.restaurantImages, - id: FacilityCardTypeEnum.restaurant, - headingText: - hotelData?.data.attributes.restaurantImages?.headingText ?? "", - heroImages: - hotelData?.data.attributes.restaurantImages?.heroImages ?? [], - }, - { - ...hotelData.data.attributes.conferencesAndMeetings, - id: FacilityCardTypeEnum.conference, - headingText: - hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "", - heroImages: - hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [], - }, - { - ...hotelData.data.attributes.healthAndWellness, - id: FacilityCardTypeEnum.wellness, - headingText: - hotelData?.data.attributes.healthAndWellness?.headingText ?? "", - heroImages: - hotelData?.data.attributes.healthAndWellness?.heroImages ?? [], - }, - ] - - return { - hotelId, - hotelName: hotelAttributes.name, - hotelDescriptions: hotelAttributes.hotelContent.texts, - hotelLocation: hotelAttributes.location, - hotelAddress: hotelAttributes.address, - hotelRatings: hotelAttributes.ratings, - hotelDetailedFacilities: hotelAttributes.detailedFacilities, - hotelImages: images, - pointsOfInterest: hotelAttributes.pointsOfInterest, - roomCategories, - activitiesCard: activities?.upcoming_activities_card, - facilities, - alerts: hotelAlerts, - faq: contentstackData?.faq, - healthFacilities: hotelAttributes.healthFacilities, - contact: hotelAttributes.contactInformation, - socials: hotelAttributes.socialMedia, - ecoLabels: hotelAttributes.hotelFacts.ecoLabels, - } - }), availability: router({ hotels: serviceProcedure .input(getHotelsAvailabilityInputSchema) diff --git a/types/components/hotelPage/facilities.ts b/types/components/hotelPage/facilities.ts index 333313fe0..eedb4e89e 100644 --- a/types/components/hotelPage/facilities.ts +++ b/types/components/hotelPage/facilities.ts @@ -4,7 +4,7 @@ import type { CardProps } from "@/components/TempDesignSystem/Card/card" export type FacilitiesProps = { facilities: Facility[] - activitiesCard?: ActivityCard + activitiesCard: ActivityCard | null } export type FacilityImage = { diff --git a/types/components/hotelPage/hotelPage.ts b/types/components/hotelPage/hotelPage.ts new file mode 100644 index 000000000..bcbb79d5d --- /dev/null +++ b/types/components/hotelPage/hotelPage.ts @@ -0,0 +1,3 @@ +export interface HotelPageProps { + hotelId: string +} From bab7c15424efccfe6e10f86ef49b3d1bb95a0e2d Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 18 Nov 2024 15:47:28 +0100 Subject: [PATCH 36/58] feat(SW-201): Added structured data for hotel pages --- components/ContentType/HotelPage/index.tsx | 19 +++-- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 4 +- i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + .../routers/contentstack/metadata/output.ts | 12 +-- server/routers/contentstack/metadata/query.ts | 24 +----- server/routers/contentstack/metadata/utils.ts | 20 +++-- server/routers/hotels/output.ts | 82 ++++++++++--------- utils/jsonSchemas.ts | 54 +++++++++++- 12 files changed, 137 insertions(+), 83 deletions(-) diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f5acca331..09143ce01 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -12,42 +12,43 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" import { getRestaurantHeading } from "@/utils/facilityCards" +import { generateHotelSchema } from "@/utils/jsonSchemas" import DynamicMap from "./Map/DynamicMap" import MapCard from "./Map/MapCard" import MapWithCardWrapper from "./Map/MapWithCard" import MobileMapToggle from "./Map/MobileMapToggle" import StaticMap from "./Map/StaticMap" -import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise" import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" import IntroSection from "./IntroSection" import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" -import { AboutTheHotelSidePeek } from "./SidePeeks" +import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks" import TabNavigation from "./TabNavigation" import styles from "./hotelPage.module.css" import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" -import { HotelPageProps } from "@/types/components/hotelPage/hotelPage" +import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" -import { Facility } from "@/types/hotel" +import type { Facility } from "@/types/hotel" export default async function HotelPage({ hotelId }: HotelPageProps) { - const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY - const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID const lang = getLang() const [intl, hotelPageData, hotelData] = await Promise.all([ getIntl(), getHotelPage(), getHotelData({ hotelId, language: lang }), ]) + const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY + const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID if (!hotelData?.data || !hotelPageData) { return notFound() } + const jsonSchema = generateHotelSchema(hotelData.data.attributes) const { faq, content } = hotelPageData const { name, @@ -103,6 +104,12 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { return (
+