diff --git a/.env b/.env deleted file mode 100644 index d116a87a8..000000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -# See update-dotenv.mjs -AUTH_URL="REPLACE-ON-NETLIFY-BUILD" -NEXTAUTH_URL="REPLACE-ON-NETLIFY-BUILD" -PUBLIC_URL="REPLACED-ON-NETLIFY-BUILD" diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts new file mode 100644 index 000000000..a0539d351 --- /dev/null +++ b/actions/registerUserBookingFlow.ts @@ -0,0 +1,67 @@ +"use server" + +import { parsePhoneNumber } from "libphonenumber-js" +import { z } from "zod" + +import { serviceServerActionProcedure } from "@/server/trpc" + +import { phoneValidator } from "@/utils/phoneValidator" + +const registerUserPayload = z.object({ + firstName: z.string(), + lastName: z.string(), + dateOfBirth: z.string(), + address: z.object({ + countryCode: z.string(), + zipCode: z.string(), + }), + email: z.string(), + phoneNumber: phoneValidator("Phone is required"), +}) + +export const registerUserBookingFlow = serviceServerActionProcedure + .input(registerUserPayload) + .mutation(async function ({ ctx, input }) { + const payload = { + ...input, + language: ctx.lang, + phoneNumber: parsePhoneNumber(input.phoneNumber) + .formatNational() + .replace(/\s+/g, ""), + } + + // TODO: Consume the API to register the user as soon as passwordless signup is enabled. + // let apiResponse + // try { + // apiResponse = await api.post(api.endpoints.v1.profile, { + // body: payload, + // headers: { + // Authorization: `Bearer ${ctx.serviceToken}`, + // }, + // }) + // } catch (error) { + // console.error("Unexpected error", error) + // return { success: false, error: "Unexpected error" } + // } + + // if (!apiResponse.ok) { + // const text = await apiResponse.text() + // console.error(text) + // console.error( + // "registerUserBookingFlow api error", + // JSON.stringify({ + // query: input, + // error: { + // status: apiResponse.status, + // statusText: apiResponse.statusText, + // error: text, + // }, + // }) + // ) + // return { success: false, error: "API error" } + // } + // const json = await apiResponse.json() + // console.log("registerUserBookingFlow: json", json) + + return { success: true, data: payload } + }) diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index 2bb2f0a52..db32e1c48 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -4,6 +4,7 @@ import { AuthError } from "next-auth" import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { signOut } from "@/auth" @@ -11,6 +12,8 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { + const publicURL = getPublicURL(request) + let redirectTo: string = "" const returnUrl = request.headers.get("x-returnurl") @@ -39,7 +42,7 @@ export async function GET( // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { console.log(`[logout] make redirectTo absolute, from ${redirectTo}`) - redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + redirectTo = new URL(redirectTo, publicURL).href console.log(`[logout] make redirectTo absolute, to ${redirectTo}`) } diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx index 1cc5c648f..43f1d66ef 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx @@ -1,6 +1,4 @@ -import { ArrowRightIcon } from "@/components/Icons" import ManagePreferencesButton from "@/components/Profile/ManagePreferencesButton" -import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index 73496fe4e..13512d701 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -1,3 +1,5 @@ +import { env } from "@/env/server" + import Divider from "@/components/TempDesignSystem/Divider" import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout" @@ -15,7 +17,7 @@ export default function ProfileLayout({ {profile} {creditCards} - {communication} + {env.HIDE_FOR_NEXT_RELEASE ? null : communication} ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx index 0e075b594..5f62c69c9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx @@ -4,6 +4,7 @@ import { serverClient } from "@/lib/trpc/server" import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" import Summary from "@/components/HotelReservation/EnterDetails/Summary" import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import { setLang } from "@/i18n/serverContext" @@ -38,6 +39,7 @@ export default async function StepLayout({ + ) diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 6c3a45365..fdd5436f3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -1,5 +1,5 @@ +import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" -import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" import { setLang } from "@/i18n/serverContext" @@ -15,18 +15,20 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - const hotelData = await serverClient().hotel.hotelData.get({ - hotelId: searchParams.hotel, - language: params.lang, - include: ["RoomCategories"], - }) - - const roomConfigurations = await serverClient().hotel.availability.rooms({ - hotelId: parseInt(searchParams.hotel, 10), - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, - }) + const [hotelData, roomConfigurations, user] = await Promise.all([ + serverClient().hotel.hotelData.get({ + hotelId: searchParams.hotel, + language: params.lang, + include: ["RoomCategories"], + }), + serverClient().hotel.availability.rooms({ + hotelId: parseInt(searchParams.hotel, 10), + roomStayStartDate: "2024-11-02", + roomStayEndDate: "2024-11-03", + adults: 1, + }), + getProfileSafely(), + ]) if (!roomConfigurations) { return "No rooms found" // TODO: Add a proper error message @@ -47,6 +49,7 @@ export default async function SelectRatePage({ diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts index b038205e6..2d91c0d5f 100644 --- a/app/[lang]/(live)/(public)/login/route.ts +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -4,6 +4,7 @@ import { AuthError } from "next-auth" import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { signIn } from "@/auth" @@ -11,9 +12,7 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } + const publicURL = getPublicURL(request) let redirectHeaders: Headers | undefined = undefined let redirectTo: string @@ -54,7 +53,7 @@ export async function GET( // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { console.log(`[login] make redirectTo absolute, from ${redirectTo}`) - redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + redirectTo = new URL(redirectTo, publicURL).href console.log(`[login] make redirectTo absolute, to ${redirectTo}`) } @@ -131,7 +130,7 @@ export async function GET( * because user might choose to do Email link login. * */ // The `for_origin` param is used to make Curity email login functionality working. - for_origin: env.PUBLIC_URL, + for_origin: publicURL, // This is new param set for differentiate between the Magic link login of New web and current web version: "2", } diff --git a/app/[lang]/(live)/(public)/verifymagiclink/route.ts b/app/[lang]/(live)/(public)/verifymagiclink/route.ts index 12c4b132f..81fa277c3 100644 --- a/app/[lang]/(live)/(public)/verifymagiclink/route.ts +++ b/app/[lang]/(live)/(public)/verifymagiclink/route.ts @@ -5,6 +5,7 @@ import { Lang } from "@/constants/languages" import { login } from "@/constants/routes/handleAuth" import { env } from "@/env/server" import { badRequest, internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { signIn } from "@/auth" @@ -12,9 +13,7 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } + const publicURL = getPublicURL(request) const loginKey = request.nextUrl.searchParams.get("loginKey") if (!loginKey) { @@ -44,7 +43,7 @@ export async function GET( console.log( `[verifymagiclink] make redirectTo absolute, from ${redirectTo}` ) - redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + redirectTo = new URL(redirectTo, publicURL).href console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`) } @@ -69,7 +68,7 @@ export async function GET( ui_locales: context.params.lang, scope: ["openid", "profile"].join(" "), loginKey: loginKey, - for_origin: env.PUBLIC_URL, + for_origin: publicURL, acr_values: "abc", version: "2", } diff --git a/app/api/web/add-card-callback/[lang]/route.ts b/app/api/web/add-card-callback/[lang]/route.ts index 63eb19942..e664124ed 100644 --- a/app/api/web/add-card-callback/[lang]/route.ts +++ b/app/api/web/add-card-callback/[lang]/route.ts @@ -4,14 +4,17 @@ import { env } from "process" import { Lang } from "@/constants/languages" import { profile } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" +import { getPublicURL } from "@/server/utils" export async function GET( request: NextRequest, { params }: { params: { lang: string } } ) { + const publicURL = getPublicURL(request) + console.log(`[add-card] callback started`) const lang = params.lang as Lang - const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`) + const returnUrl = new URL(`${publicURL}/${profile[lang ?? Lang.en]}`) try { const searchParams = request.nextUrl.searchParams diff --git a/app/api/web/envtest-dynamic-edge/route.ts b/app/api/web/envtest-dynamic-edge/route.ts deleted file mode 100644 index d2a12f7bf..000000000 --- a/app/api/web/envtest-dynamic-edge/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" - -export const runtime = "edge" diff --git a/app/api/web/envtest-dynamic-load-file/route.ts b/app/api/web/envtest-dynamic-load-file/route.ts deleted file mode 100644 index 115f12f77..000000000 --- a/app/api/web/envtest-dynamic-load-file/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ path: "./.env" }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-dynamic-load/route.ts b/app/api/web/envtest-dynamic-load/route.ts deleted file mode 100644 index 18c964526..000000000 --- a/app/api/web/envtest-dynamic-load/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import "dotenv/config" - -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-dynamic-override/route.ts b/app/api/web/envtest-dynamic-override/route.ts deleted file mode 100644 index 4d45b8e5f..000000000 --- a/app/api/web/envtest-dynamic-override/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ debug: true, override: true }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-dynamic/route.ts b/app/api/web/envtest-dynamic/route.ts deleted file mode 100644 index 386817938..000000000 --- a/app/api/web/envtest-dynamic/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-static-load-file/route.ts b/app/api/web/envtest-static-load-file/route.ts deleted file mode 100644 index f1a83978a..000000000 --- a/app/api/web/envtest-static-load-file/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ path: "./.env" }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/envtest-static-load/route.ts b/app/api/web/envtest-static-load/route.ts deleted file mode 100644 index 4e360a891..000000000 --- a/app/api/web/envtest-static-load/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import "dotenv/config" - -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/envtest-static-override/route.ts b/app/api/web/envtest-static-override/route.ts deleted file mode 100644 index d844016c5..000000000 --- a/app/api/web/envtest-static-override/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ debug: true, override: true }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/envtest-static/route.ts b/app/api/web/envtest-static/route.ts deleted file mode 100644 index 1c71b77ca..000000000 --- a/app/api/web/envtest-static/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts index 0b8133c43..4154aff14 100644 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -6,20 +6,21 @@ import { bookingConfirmation, payment, } from "@/constants/routes/hotelReservation" +import { getPublicURL } from "@/server/utils" export async function GET( request: NextRequest, { params }: { params: { lang: string; status: string } } ): Promise { + const publicURL = getPublicURL(request) + console.log(`[payment-callback] callback started`) const lang = params.lang as Lang const status = params.status - const returnUrl = new URL(`${env.PUBLIC_URL}/${payment[lang]}`) + const returnUrl = new URL(`${publicURL}/${payment[lang]}`) if (status === "success") { - const confirmationUrl = new URL( - `${env.PUBLIC_URL}/${bookingConfirmation[lang]}` - ) + const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`) console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) return NextResponse.redirect(confirmationUrl) } diff --git a/auth.ts b/auth.ts index 0de343626..3adc360f8 100644 --- a/auth.ts +++ b/auth.ts @@ -109,6 +109,7 @@ const curityProvider = { } satisfies OIDCConfig export const config = { + basePath: "/api/web/auth", debug: env.NEXTAUTH_DEBUG, providers: [curityProvider], redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL, @@ -233,4 +234,4 @@ export const { auth, signIn, signOut, -} = NextAuth(config) \ No newline at end of file +} = NextAuth(config) diff --git a/components/Blocks/Accordion/accordion.module.css b/components/Blocks/Accordion/accordion.module.css new file mode 100644 index 000000000..0dd49a0b7 --- /dev/null +++ b/components/Blocks/Accordion/accordion.module.css @@ -0,0 +1,7 @@ +.accordion:not(.allVisible) :nth-child(n + 6) { + display: none; +} + +.accordion:not(.allVisible) :nth-child(5) { + border: none; +} diff --git a/components/Blocks/Accordion/index.tsx b/components/Blocks/Accordion/index.tsx new file mode 100644 index 000000000..8828555d1 --- /dev/null +++ b/components/Blocks/Accordion/index.tsx @@ -0,0 +1,53 @@ +"use client" +import { useState } from "react" + +import JsonToHtml from "@/components/JsonToHtml" +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import Accordion from "@/components/TempDesignSystem/Accordion" +import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" +import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" + +import styles from "./accordion.module.css" + +import type { AccordionProps } from "@/types/components/blocks/Accordion" +import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" + +export default function AccordionSection({ accordion, title }: AccordionProps) { + const showToggleButton = accordion.length > 5 + const [allAccordionsVisible, setAllAccordionsVisible] = + useState(!showToggleButton) + + function toggleAccordions() { + setAllAccordionsVisible((state) => !state) + } + + return ( + + {title && } + + {accordion.map((acc) => ( + + + + ))} + + + {showToggleButton ? ( + + ) : null} + + ) +} diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 45a500d20..1c07b73ec 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -7,13 +7,27 @@ import TeaserCard from "@/components/TempDesignSystem/TeaserCard" import type { CardsGridProps } from "@/types/components/blocks/cardsGrid" import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid" +import type { StackableGridProps } from "../TempDesignSystem/Grids/Stackable/stackable" export default function CardsGrid({ cards_grid, firstItem = false, }: CardsGridProps) { - const columns = - cards_grid.layout === CardsGridLayoutEnum.THREE_COLUMNS ? 3 : 2 + let columns: StackableGridProps["columns"] + + switch (cards_grid.layout) { + case CardsGridLayoutEnum.ONE_COLUMN: + columns = 1 + break + case CardsGridLayoutEnum.TWO_COLUMNS: + columns = 2 + break + case CardsGridLayoutEnum.THREE_COLUMNS: + columns = 3 + break + default: + columns = 3 + } return ( diff --git a/components/Blocks/DynamicContent/index.tsx b/components/Blocks/DynamicContent/index.tsx index 31ba4009a..21b2616ac 100644 --- a/components/Blocks/DynamicContent/index.tsx +++ b/components/Blocks/DynamicContent/index.tsx @@ -1,3 +1,5 @@ +import { env } from "@/env/server" + import HowItWorks from "@/components/Blocks/DynamicContent/HowItWorks" import LoyaltyLevels from "@/components/Blocks/DynamicContent/LoyaltyLevels" import Overview from "@/components/Blocks/DynamicContent/Overview" @@ -26,7 +28,9 @@ export default async function DynamicContent({ case DynamicContentEnum.Blocks.components.earn_and_burn: return case DynamicContentEnum.Blocks.components.expiring_points: - return + return env.HIDE_FOR_NEXT_RELEASE ? null : ( + + ) case DynamicContentEnum.Blocks.components.how_it_works: return ( diff --git a/components/Blocks/UspGrid/renderOptions.tsx b/components/Blocks/UspGrid/renderOptions.tsx index a4033fec5..96a0f7fa1 100644 --- a/components/Blocks/UspGrid/renderOptions.tsx +++ b/components/Blocks/UspGrid/renderOptions.tsx @@ -1,9 +1,7 @@ import Link from "@/components/TempDesignSystem/Link" -import { removeMultipleSlashes } from "@/utils/url" import styles from "./uspgrid.module.css" -import { EmbedEnum } from "@/types/requests/utils/embeds" import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml" import { RTEItemTypeEnum, RTETypeEnum } from "@/types/transitionTypes/rte/enums" import type { diff --git a/components/Blocks/index.tsx b/components/Blocks/index.tsx index bd8abb6f7..f165b50ba 100644 --- a/components/Blocks/index.tsx +++ b/components/Blocks/index.tsx @@ -5,6 +5,7 @@ import TextCols from "@/components/Blocks/TextCols" import UspGrid from "@/components/Blocks/UspGrid" import JsonToHtml from "@/components/JsonToHtml" +import AccordionSection from "./Accordion" import Table from "./Table" import type { BlocksProps } from "@/types/components/blocks" @@ -14,6 +15,14 @@ export default function Blocks({ blocks }: BlocksProps) { return blocks.map((block, idx) => { const firstItem = idx === 0 switch (block.typename) { + case BlocksEnums.block.Accordion: + return ( + + ) case BlocksEnums.block.CardsGrid: return ( + default: return null } diff --git a/components/Breadcrumbs/index.tsx b/components/Breadcrumbs/index.tsx index 752f77d5f..1939e47fa 100644 --- a/components/Breadcrumbs/index.tsx +++ b/components/Breadcrumbs/index.tsx @@ -48,7 +48,7 @@ export default async function Breadcrumbs() { return (
  • - + {breadcrumb.title}
  • diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index f6118a2fc..360d0f8df 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -11,10 +11,10 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { IntroSectionProps } from "./types" - import styles from "./introSection.module.css" +import type { IntroSectionProps } from "./types" + export default async function IntroSection({ hotelName, hotelDescription, diff --git a/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx b/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx index a04082702..6dbd27425 100644 --- a/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx +++ b/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx @@ -1,5 +1,4 @@ "use client" - import { useIntl } from "react-intl" import useHotelPageStore from "@/stores/hotel-page" @@ -21,7 +20,6 @@ export default function MobileMapToggle() { onClick={closeDynamicMap} > 3 + const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton) + const scrollRef = useRef(null) const mappedRooms = rooms @@ -42,12 +43,11 @@ export function Rooms({ rooms }: RoomsProps) { }) .sort((a, b) => a.sortOrder - b.sortOrder) - function handleToggleShowMore() { + function handleShowMore() { if (scrollRef.current && allRoomsVisible) { scrollRef.current.scrollIntoView({ behavior: "smooth" }) } - - setAllRoomsVisible((previousState) => !previousState) + setAllRoomsVisible((state) => !state) } return ( @@ -61,41 +61,30 @@ export function Rooms({ rooms }: RoomsProps) { title={intl.formatMessage({ id: "Rooms" })} preamble={null} /> - - {mappedRooms.map( - ({ id, images, title, subtitle, popularChoice }, index) => ( -
    2 ? styles.hiddenRoomCard : "" - } - > - -
    - ) - )} + + {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( +
    + +
    + ))}
    -
    - -
    + {showToggleButton ? ( + + ) : null}
    ) } diff --git a/components/ContentType/HotelPage/Rooms/rooms.module.css b/components/ContentType/HotelPage/Rooms/rooms.module.css index 1f719b1d2..7e9ad6ac5 100644 --- a/components/ContentType/HotelPage/Rooms/rooms.module.css +++ b/components/ContentType/HotelPage/Rooms/rooms.module.css @@ -13,10 +13,10 @@ justify-content: center; } -.hiddenRoomCard { - display: none; -} - .showMoreButton.showLess .chevron { transform: rotate(180deg); } + +.grid:not(.allVisible) :nth-child(n + 4) { + display: none; +} diff --git a/components/ContentType/HotelPage/Rooms/types.ts b/components/ContentType/HotelPage/Rooms/types.ts deleted file mode 100644 index b4c320af2..000000000 --- a/components/ContentType/HotelPage/Rooms/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RoomData } from "@/types/hotel" - -export type RoomsProps = { - rooms: RoomData[] -} diff --git a/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css b/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css index 1c4b051c9..702f7d1fc 100644 --- a/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css +++ b/components/ContentType/HotelPage/TabNavigation/tabNavigation.module.css @@ -1,7 +1,7 @@ .stickyWrapper { position: sticky; top: var(--booking-widget-mobile-height); - z-index: 1; + z-index: 2; background-color: var(--Base-Surface-Subtle-Normal); border-bottom: 1px solid var(--Base-Border-Subtle); overflow-x: auto; diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 771c4e76e..09f900159 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -11,6 +11,7 @@ "mapContainer"; margin: 0 auto; max-width: var(--max-width); + z-index: 0; } .hotelImages { diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 8976ead74..5b0139d6f 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -2,6 +2,7 @@ import hotelPageParams from "@/constants/routes/hotelPageParams" import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" +import AccordionSection from "@/components/Blocks/Accordion" import SidePeekProvider from "@/components/SidePeekProvider" import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" @@ -47,6 +48,7 @@ export default async function HotelPage() { activitiesCard, pointsOfInterest, facilities, + faq, } = hotelData const topThreePois = pointsOfInterest.slice(0, 3) @@ -64,7 +66,7 @@ export default async function HotelPage() {
    @@ -80,6 +82,9 @@ export default async function HotelPage() {
    + {faq && ( + + )}
    {googleMapsApiKey ? ( <> diff --git a/components/DatePicker/Screen/Desktop.tsx b/components/DatePicker/Screen/Desktop.tsx index db290137e..9056f9183 100644 --- a/components/DatePicker/Screen/Desktop.tsx +++ b/components/DatePicker/Screen/Desktop.tsx @@ -61,9 +61,9 @@ export default function DatePickerDesktop({ locale={locale} mode="range" numberOfMonths={2} - onSelect={handleOnSelect} + onDayClick={handleOnSelect} pagedNavigation - required + required={false} selected={selectedDate} startMonth={currentDate} weekStartsOn={1} @@ -82,7 +82,7 @@ export default function DatePickerDesktop({ size="small" theme="base" > - + {intl.formatMessage({ id: "Select dates" })} diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx index 3d367d995..0407f9b63 100644 --- a/components/DatePicker/Screen/Mobile.tsx +++ b/components/DatePicker/Screen/Mobile.tsx @@ -78,7 +78,7 @@ export default function DatePickerMobile({ mode="range" /** Showing full year or what's left of it */ numberOfMonths={12} - onSelect={handleOnSelect} + onDayClick={handleOnSelect} required selected={selectedDate} startMonth={startMonth} diff --git a/components/DatePicker/Screen/desktop.module.css b/components/DatePicker/Screen/desktop.module.css index afbb286a7..aa02537a5 100644 --- a/components/DatePicker/Screen/desktop.module.css +++ b/components/DatePicker/Screen/desktop.module.css @@ -47,8 +47,8 @@ td.rangeStart[aria-selected="true"] button.dayButton:hover { } td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, -td.rangeStart[aria-selected="true"]:not([data-outside="true"]) - button.dayButton { +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.day[aria-selected="true"] button.dayButton { background: var(--Primary-Light-On-Surface-Accent); border: none; color: var(--Base-Button-Inverted-Fill-Normal); @@ -75,6 +75,7 @@ td.rangeMiddle[aria-selected="true"] button.dayButton { background: var(--Base-Background-Primary-Normal); border: none; border-radius: 0; + color: var(--UI-Text-High-contrast); } td.day[data-disabled="true"], diff --git a/components/DatePicker/Screen/mobile.module.css b/components/DatePicker/Screen/mobile.module.css index 4f820fd5f..ef3f97ca0 100644 --- a/components/DatePicker/Screen/mobile.module.css +++ b/components/DatePicker/Screen/mobile.module.css @@ -113,8 +113,8 @@ td.rangeStart[aria-selected="true"] button.dayButton:hover { } td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, -td.rangeStart[aria-selected="true"]:not([data-outside="true"]) - button.dayButton { +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.day[aria-selected="true"] button.dayButton { background: var(--Primary-Light-On-Surface-Accent); border: none; color: var(--Base-Button-Inverted-Fill-Normal); @@ -141,6 +141,7 @@ td.rangeMiddle[aria-selected="true"] button.dayButton { background: var(--Base-Background-Primary-Normal); border: none; border-radius: 0; + color: var(--UI-Text-High-contrast); } td.day[data-disabled="true"], diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css index b11ab3b77..b2e23cd41 100644 --- a/components/DatePicker/date-picker.module.css +++ b/components/DatePicker/date-picker.module.css @@ -22,6 +22,11 @@ .hideWrapper { background-color: var(--Main-Grey-White); + display: none; +} + +.container[data-isopen="true"] .hideWrapper { + display: block; } @media screen and (max-width: 1366px) { diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index 586da5208..503c0c6ac 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -14,8 +14,6 @@ import DatePickerMobile from "./Screen/Mobile" import styles from "./date-picker.module.css" -import type { DateRange } from "react-day-picker" - import type { DatePickerFormProps } from "@/types/components/datepicker" const locales = { @@ -33,6 +31,8 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const { register, setValue } = useFormContext() const ref = useRef(null) + const [isSelectingFrom, setIsSelectingFrom] = useState(true) + function close() { setIsOpen(false) } @@ -41,11 +41,29 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { setIsOpen((prevIsOpen) => !prevIsOpen) } - function handleSelectDate(selected: DateRange) { - setValue(name, { - from: dt(selected.from).format("YYYY-MM-DD"), - to: dt(selected.to).format("YYYY-MM-DD"), - }) + function handleSelectDate(selected: Date) { + if (isSelectingFrom) { + setValue(name, { + from: dt(selected).format("YYYY-MM-DD"), + to: undefined, + }) + setIsSelectingFrom(false) + } else { + const fromDate = dt(selectedDate.from) + const toDate = dt(selected) + if (toDate.isAfter(fromDate)) { + setValue(name, { + from: selectedDate.from, + to: toDate.format("YYYY-MM-DD"), + }) + } else { + setValue(name, { + from: toDate.format("YYYY-MM-DD"), + to: selectedDate.from, + }) + } + setIsSelectingFrom(true) + } } useEffect(() => { @@ -64,7 +82,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const selectedFromDate = dt(selectedDate.from) .locale(lang) .format("ddd D MMM") - const selectedToDate = dt(selectedDate.to).locale(lang).format("ddd D MMM") + const selectedToDate = !!selectedDate.to + ? dt(selectedDate.to).locale(lang).format("ddd D MMM") + : "" return (
    diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx index 116934531..3b4391998 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx @@ -33,7 +33,7 @@ export default function ClearSearchButton({ type="button" > - + {intl.formatMessage({ id: "Clear searches" })} diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index af5f7ce3d..12d5e740c 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -48,15 +48,6 @@ export default function Search({ locations }: SearchProps) { dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS }) } - function handleOnBlur() { - if (!value && state.searchData?.name) { - setValue(name, state.searchData.name) - // Always need to manually trigger - // revalidation when setting value r-h-f - trigger() - } - } - function handleOnChange( evt: FormEvent | ChangeEvent ) { @@ -138,7 +129,9 @@ export default function Search({ locations }: SearchProps) {
    @@ -154,7 +147,6 @@ export default function Search({ locations }: SearchProps) { }), ...register(name, { onBlur: function () { - handleOnBlur() closeMenu() }, onChange: handleOnChange, diff --git a/components/Forms/BookingWidget/FormContent/Search/search.module.css b/components/Forms/BookingWidget/FormContent/Search/search.module.css index d1acb79be..0b9ecfa8c 100644 --- a/components/Forms/BookingWidget/FormContent/Search/search.module.css +++ b/components/Forms/BookingWidget/FormContent/Search/search.module.css @@ -24,8 +24,3 @@ p { color: var(--UI-Text-Active); } - -.container:hover:has(input:not(:active, :focus, :focus-within)) - input::-webkit-search-cancel-button { - display: none; -} diff --git a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx index 0a9841a43..067f94c0c 100644 --- a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx @@ -1,7 +1,7 @@ "use client" import { useIntl } from "react-intl" -import Body from "@/components/TempDesignSystem/Text/Body" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Caption from "@/components/TempDesignSystem/Text/Caption" import { Tooltip } from "@/components/TempDesignSystem/Tooltip" @@ -34,7 +34,7 @@ export default function Voucher() { >
    @@ -60,17 +60,12 @@ export default function FormContent({
    + + {count} + + +
    + ) +} diff --git a/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx new file mode 100644 index 000000000..ee0e835b3 --- /dev/null +++ b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx @@ -0,0 +1,113 @@ +"use client" +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { useGuestsRoomsStore } from "@/stores/guests-rooms" + +import { CloseLargeIcon, PlusCircleIcon } from "../Icons" +import Button from "../TempDesignSystem/Button" +import Divider from "../TempDesignSystem/Divider" +import Subtitle from "../TempDesignSystem/Text/Subtitle" +import { Tooltip } from "../TempDesignSystem/Tooltip" +import AdultSelector from "./AdultSelector" +import ChildSelector from "./ChildSelector" + +import styles from "./guests-rooms-picker.module.css" + +import { BookingWidgetSchema } from "@/types/components/bookingWidget" +import { GuestsRoomsPickerProps } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export default function GuestsRoomsPicker({ + closePicker, +}: GuestsRoomsPickerProps) { + const intl = useIntl() + const doneLabel = intl.formatMessage({ id: "Done" }) + const roomLabel = intl.formatMessage({ id: "Room" }) + const disabledBookingOptionsHeader = intl.formatMessage({ + id: "Disabled booking options header", + }) + const disabledBookingOptionsText = intl.formatMessage({ + id: "Disabled booking options text", + }) + const addRoomLabel = intl.formatMessage({ id: "Add Room" }) + + const { getFieldState } = useFormContext() + + const rooms = useGuestsRoomsStore((state) => state.rooms) + + // Not in MVP + // const increaseRoom = useGuestsRoomsStore.use.increaseRoom() + // const decreaseRoom = useGuestsRoomsStore.use.decreaseRoom() + + return ( +
    +
    + +
    +
    + {rooms.map((room, index) => ( +
    +
    + + {roomLabel} {index + 1} + + + +
    + {/* Not in MVP + {index > 0 ? ( + + ) : null} */} + +
    + ))} +
    +
    + + {rooms.length < 4 ? ( + + ) : null} + + + +
    +
    + ) +} diff --git a/components/GuestsRoomsPicker/guests-rooms-picker.module.css b/components/GuestsRoomsPicker/guests-rooms-picker.module.css new file mode 100644 index 000000000..8e0465206 --- /dev/null +++ b/components/GuestsRoomsPicker/guests-rooms-picker.module.css @@ -0,0 +1,141 @@ +.container { + overflow: hidden; + position: relative; + &[data-isopen="true"] { + overflow: visible; + } +} +.roomContainer { + display: grid; + gap: var(--Spacing-x2); +} +.roomDetailsContainer { + display: grid; + gap: var(--Spacing-x2); + padding-bottom: var(--Spacing-x1); +} +.hideWrapper { + background-color: var(--Main-Grey-White); +} +.roomHeading { + margin-bottom: var(--Spacing-x1); +} +.btn { + background: none; + border: none; + cursor: pointer; + outline: none; + padding: 0; + width: 100%; +} +.body { + opacity: 0.8; +} +.footer { + display: grid; + gap: var(--Spacing-x1); + grid-template-columns: auto auto; + margin-top: var(--Spacing-x2); +} + +@media screen and (max-width: 1366px) { + .hideWrapper { + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 100%; + transition: top 300ms ease; + z-index: 10002; + } + + .container[data-isopen="true"] .hideWrapper { + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + top: 20px; + } + + .pickerContainer { + --header-height: 72px; + --sticky-button-height: 140px; + display: grid; + grid-template-areas: + "header" + "content"; + grid-template-rows: var(--header-height) calc(100dvh - var(--header-height)); + position: relative; + } + .contentContainer { + grid-area: content; + overflow-y: scroll; + scroll-snap-type: y mandatory; + } + + .header { + background-color: var(--Main-Grey-White); + display: grid; + grid-area: header; + padding: var(--Spacing-x3) var(--Spacing-x2); + } + + .close { + background: none; + border: none; + cursor: pointer; + display: flex; + justify-self: flex-end; + padding: 0; + } + + .roomContainer { + padding: 0 var(--Spacing-x2); + } + .roomContainer:last-of-type { + padding-bottom: calc(var(--sticky-button-height) + 20px); + } + + .footer { + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 7.5%, + #ffffff 82.5% + ); + padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7); + position: sticky; + bottom: 0; + width: 100%; + z-index: 10; + } + + .footer button { + width: 100%; + } + + .footer .hideOnMobile { + display: none; + } + + .footer .addRoom { + justify-content: start; + } +} + +@media screen and (min-width: 1367px) { + .hideWrapper { + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + left: calc((var(--Spacing-x1) + var(--Spacing-x2)) * -1); + max-width: calc(100vw - 20px); + padding: var(--Spacing-x2) var(--Spacing-x3); + position: absolute; + top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4)); + width: 360px; + } + + .header { + display: none; + } + + .footer .hideOnDesktop { + display: none; + } +} diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx new file mode 100644 index 000000000..f12ecebde --- /dev/null +++ b/components/GuestsRoomsPicker/index.tsx @@ -0,0 +1,80 @@ +"use client" + +import { useCallback, useEffect, useRef, useState } from "react" +import { useIntl } from "react-intl" + +import { useGuestsRoomsStore } from "@/stores/guests-rooms" + +import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema" +import Body from "@/components/TempDesignSystem/Text/Body" + +import GuestsRoomsPicker from "./GuestsRoomsPicker" + +import styles from "./guests-rooms-picker.module.css" + +export default function GuestsRoomsPickerForm() { + const intl = useIntl() + const [isOpen, setIsOpen] = useState(false) + const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore( + (state) => ({ + rooms: state.rooms, + adultCount: state.adultCount, + childCount: state.childCount, + setIsValidated: state.setIsValidated, + }) + ) + const ref = useRef(null) + function handleOnClick() { + setIsOpen((prevIsOpen) => !prevIsOpen) + } + const closePicker = useCallback(() => { + const guestRoomsValidData = guestRoomsSchema.safeParse(rooms) + if (guestRoomsValidData.success) { + setIsOpen(false) + setIsValidated(false) + } else { + setIsValidated(true) + } + }, [rooms, setIsValidated, setIsOpen]) + + useEffect(() => { + function handleClickOutside(evt: Event) { + const target = evt.target as HTMLElement + if (ref.current && target && !ref.current.contains(target)) { + closePicker() + } + } + document.addEventListener("click", handleClickOutside) + return () => { + document.removeEventListener("click", handleClickOutside) + } + }, [closePicker]) + + return ( +
    + +
    + +
    +
    + ) +} diff --git a/components/HotelReservation/ReadMore/Contact/contact.module.css b/components/HotelReservation/Contact/contact.module.css similarity index 93% rename from components/HotelReservation/ReadMore/Contact/contact.module.css rename to components/HotelReservation/Contact/contact.module.css index 53f6e01f3..768e01ca2 100644 --- a/components/HotelReservation/ReadMore/Contact/contact.module.css +++ b/components/HotelReservation/Contact/contact.module.css @@ -32,8 +32,8 @@ } .ecoLabel { - display: grid; - grid-template-columns: auto 1fr; + display: flex; + align-items: center; column-gap: var(--Spacing-x-one-and-half); grid-column: 2 / 3; grid-row: 3 / 4; diff --git a/components/HotelReservation/ReadMore/Contact/index.tsx b/components/HotelReservation/Contact/index.tsx similarity index 84% rename from components/HotelReservation/ReadMore/Contact/index.tsx rename to components/HotelReservation/Contact/index.tsx index 46491fb3d..2f4e8ccd5 100644 --- a/components/HotelReservation/ReadMore/Contact/index.tsx +++ b/components/HotelReservation/Contact/index.tsx @@ -24,20 +24,26 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Address" })} - {hotel.address.streetAddress} - {hotel.address.city} + + {`${hotel.address.streetAddress}, ${hotel.address.city}`} +
  • {intl.formatMessage({ id: "Driving directions" })} - {intl.formatMessage({ id: "Google Maps" })} + + Google Maps +
  • {intl.formatMessage({ id: "Email" })} - + {hotel.contactInformation.email}
  • @@ -45,7 +51,10 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Contact us" })} - + {hotel.contactInformation.phoneNumber} diff --git a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx new file mode 100644 index 000000000..74a17e911 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx @@ -0,0 +1,97 @@ +"use client" + +import { useEffect, useState } from "react" +import { useWatch } from "react-hook-form" +import { useIntl } from "react-intl" + +import { privacyPolicy } from "@/constants/currentWebHrefs" + +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox" +import DateSelect from "@/components/TempDesignSystem/Form/Date" +import Input from "@/components/TempDesignSystem/Form/Input" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import useLang from "@/hooks/useLang" + +import styles from "./signup.module.css" + +export default function Signup({ name }: { name: string }) { + const lang = useLang() + const intl = useIntl() + + const [isJoinChecked, setIsJoinChecked] = useState(false) + + const joinValue = useWatch({ name }) + + useEffect(() => { + // In order to avoid hydration errors the state needs to be set as side effect, + // since the join value can come from search params + setIsJoinChecked(joinValue) + }, [joinValue]) + + const list = [ + { title: intl.formatMessage({ id: "Earn bonus nights & points" }) }, + { title: intl.formatMessage({ id: "Get member benefits & offers" }) }, + { title: intl.formatMessage({ id: "Join at no cost" }) }, + ] + + return ( +
    + + {isJoinChecked ? ( +
    +
    +
    + + {intl.formatMessage({ id: "Birth date" })} * + +
    + + +
    +
    + + + {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", + })}{" "} + + {intl.formatMessage({ id: "Scandic's Privacy Policy." })} + + + +
    +
    + ) : null} +
    + ) +} diff --git a/components/HotelReservation/EnterDetails/Details/Signup/signup.module.css b/components/HotelReservation/EnterDetails/Details/Signup/signup.module.css new file mode 100644 index 000000000..1db11dfb8 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Details/Signup/signup.module.css @@ -0,0 +1,15 @@ +.container { + display: grid; + grid-column: 1/-1; + gap: var(--Spacing-x3); +} + +.additionalFormData { + display: grid; + gap: var(--Spacing-x4); +} + +.dateField { + display: grid; + gap: var(--Spacing-x1); +} diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 8f034f501..35c16304b 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -6,14 +6,16 @@ import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" +import { registerUserBookingFlow } from "@/actions/registerUserBookingFlow" import Button from "@/components/TempDesignSystem/Button" -import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" import Phone from "@/components/TempDesignSystem/Form/Phone" import Body from "@/components/TempDesignSystem/Text/Body" +import { toast } from "@/components/TempDesignSystem/Toasts" import { detailsSchema, signedInDetailsSchema } from "./schema" +import Signup from "./Signup" import styles from "./details.module.css" @@ -25,28 +27,30 @@ import type { const formID = "enter-details" export default function Details({ user }: DetailsProps) { const intl = useIntl() - - const list = [ - { title: intl.formatMessage({ id: "Earn bonus nights & points" }) }, - { title: intl.formatMessage({ id: "Get member benefits & offers" }) }, - { title: intl.formatMessage({ id: "Join at no cost" }) }, - ] - const initialData = useEnterDetailsStore((state) => ({ countryCode: state.data.countryCode, email: state.data.email, - firstname: state.data.firstname, - lastname: state.data.lastname, + firstName: state.data.firstName, + lastName: state.data.lastName, phoneNumber: state.data.phoneNumber, + join: state.data.join, + dateOfBirth: state.data.dateOfBirth, + zipCode: state.data.zipCode, + termsAccepted: state.data.termsAccepted, })) const methods = useForm({ defaultValues: { countryCode: user?.address?.countryCode ?? initialData.countryCode, email: user?.email ?? initialData.email, - firstname: user?.firstName ?? initialData.firstname, - lastname: user?.lastName ?? initialData.lastname, + firstName: user?.firstName ?? initialData.firstName, + lastName: user?.lastName ?? initialData.lastName, phoneNumber: user?.phoneNumber ?? initialData.phoneNumber, + //@ts-expect-error: We use a literal for join to be true or false, which does not convert to a boolean + join: initialData.join, + dateOfBirth: initialData.dateOfBirth, + zipCode: initialData.zipCode, + termsAccepted: initialData.termsAccepted, }, criteriaMode: "all", mode: "all", @@ -56,10 +60,39 @@ export default function Details({ user }: DetailsProps) { const completeStep = useEnterDetailsStore((state) => state.completeStep) + // const errorMessage = intl.formatMessage({ + // id: "An error occurred. Please try again.", + // }) + const onSubmit = useCallback( - (values: DetailsSchema) => { + async function (values: DetailsSchema) { + if (values.join) { + const signupVals = { + firstName: values.firstName, + lastName: values.lastName, + email: values.email, + phoneNumber: values.phoneNumber, + address: { + zipCode: values.zipCode, + countryCode: values.countryCode, + }, + dateOfBirth: values.dateOfBirth, + } + + const res = await registerUserBookingFlow(signupVals) + if (!res.success) { + // if (res.error) { + // toast.error(res.error) + // } else { + // toast.error(errorMessage) + // } + return + } + console.log("Signed up user: ", res) + } completeStep(values) }, + [completeStep] ) @@ -77,14 +110,14 @@ export default function Details({ user }: DetailsProps) { onSubmit={methods.handleSubmit(onSubmit)} > @@ -109,26 +142,9 @@ export default function Details({ user }: DetailsProps) { readOnly={!!user} registerOptions={{ required: true }} /> + {user ? null : }
    - {user ? null : ( - - )} )} -
    - {children} -
    +
    {children}
    ) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 11598c8bd..3680f5628 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -22,12 +22,14 @@ } .main { - display: flex; - flex-direction: column; + display: grid; gap: var(--Spacing-x3); width: 100%; border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-bottom: var(--Spacing-x3); + + transition: 0.4s ease-out; + grid-template-rows: 2em 0fr; } .headerContainer { @@ -70,12 +72,23 @@ background-color: var(--Base-Surface-Subtle-Hover); } +.wrapper[data-open="true"] .main { + grid-template-rows: 2em 1fr; +} + .content { overflow: hidden; - transition: max-height 0.4s ease-out; - max-height: 0; +} + +@keyframes allowOverflow { + 0% { + overflow: hidden; + } + 100% { + overflow: visible; + } } .wrapper[data-open="true"] .content { - max-height: 1000px; + animation: allowOverflow 0.4s 0.4s ease; } diff --git a/components/HotelReservation/EnterDetails/SidePeek/enterDetailsSidePeek.module.css b/components/HotelReservation/EnterDetails/SidePeek/enterDetailsSidePeek.module.css new file mode 100644 index 000000000..be80e5dbd --- /dev/null +++ b/components/HotelReservation/EnterDetails/SidePeek/enterDetailsSidePeek.module.css @@ -0,0 +1,5 @@ +.spacing { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} diff --git a/components/HotelReservation/EnterDetails/SidePeek/index.tsx b/components/HotelReservation/EnterDetails/SidePeek/index.tsx new file mode 100644 index 000000000..abbad2dec --- /dev/null +++ b/components/HotelReservation/EnterDetails/SidePeek/index.tsx @@ -0,0 +1,42 @@ +"use client" + +import { useIntl } from "react-intl" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +import Contact from "@/components/HotelReservation/Contact" +import Divider from "@/components/TempDesignSystem/Divider" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" + +import styles from "./enterDetailsSidePeek.module.css" + +import { + SidePeekEnum, + SidePeekProps, +} from "@/types/components/enterDetails/sidePeek" + +export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) { + const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek) + const close = useEnterDetailsStore((state) => state.closeSidePeek) + + const intl = useIntl() + return ( + +
    + + +
    + {hotel.hotelContent.texts.descriptions.medium} + + {hotel.hotelContent.texts.facilityInformation} +
    +
    +
    + ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx b/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx new file mode 100644 index 000000000..2f022f857 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useIntl } from "react-intl" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +import { ChevronRightSmallIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" + +export default function ToggleSidePeek() { + const intl = useIntl() + const openSidePeek = useEnterDetailsStore((state) => state.openSidePeek) + + return ( + + ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 8efc418a1..18121dad3 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -1,13 +1,14 @@ import { dt } from "@/lib/dt" -import { ArrowRightIcon, ChevronRightSmallIcon } from "@/components/Icons" +import { ArrowRightIcon } from "@/components/Icons" import Divider from "@/components/TempDesignSystem/Divider" -import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import ToggleSidePeek from "./ToggleSidePeek" + import styles from "./summary.module.css" // TEMP @@ -21,7 +22,6 @@ const rooms = [ export default async function Summary() { const intl = await getIntl() const lang = getLang() - const fromDate = dt().locale(lang).format("ddd, D MMM") const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM") const diff = dt(toDate).diff(fromDate, "days") @@ -75,19 +75,8 @@ export default async function Summary() { {toDate} - - {intl.formatMessage({ id: "See room details" })} - - + +
    diff --git a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/hotelDetailSidePeek.module.css b/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/hotelDetailSidePeek.module.css deleted file mode 100644 index fb400465e..000000000 --- a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/hotelDetailSidePeek.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.buttons { - display: flex; - gap: var(--Spacing-x3); -} diff --git a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/index.tsx b/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/index.tsx deleted file mode 100644 index d4e3eb829..000000000 --- a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import { useState } from "react" -import { useIntl } from "react-intl" - -import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall" -import Button from "@/components/TempDesignSystem/Button" -import SidePeek from "@/components/TempDesignSystem/SidePeek" - -import styles from "./hotelDetailSidePeek.module.css" - -export default function HotelDetailSidePeek() { - const intl = useIntl() - const [isOpen, setIsOpen] = useState(false) - - function toggleSidePeek() { - setIsOpen(!isOpen) - } - - return ( - <> -
    - - -
    - setIsOpen(false)} - > -
    TBD
    -
    - - ) -} diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/components/HotelReservation/HotelSelectionHeader/index.tsx index 222dceda1..69b4134eb 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/components/HotelReservation/HotelSelectionHeader/index.tsx @@ -6,8 +6,6 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" -import HotelDetailSidePeek from "./HotelDetailSidePeek" - import styles from "./hotelSelectionHeader.module.css" import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader" @@ -46,7 +44,6 @@ export default function HotelSelectionHeader({ {hotel.hotelContent.texts.descriptions.short} -
    diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index e988ec40d..5814eae54 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -11,7 +11,7 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Contact from "./Contact" +import Contact from "../Contact" import styles from "./readMore.module.css" diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx index 952d8add3..76f366adb 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -26,7 +26,7 @@ export default function PriceList({
    {intl.formatMessage({ id: "Standard price" })} @@ -52,10 +52,7 @@ export default function PriceList({
    - + {intl.formatMessage({ id: "Member price" })}
    diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 481b79e98..3086a5c2b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -17,6 +17,8 @@ export default function FlexibilityOption({ name, paymentTerm, priceInformation, + roomType, + handleSelectRate, }: FlexibilityOptionProps) { const [rootDiv, setRootDiv] = useState(undefined) const [isPopoverOpen, setIsPopoverOpen] = useState(false) @@ -42,9 +44,24 @@ export default function FlexibilityOption({ const { public: publicPrice, member: memberPrice } = product.productType + function onChange() { + const rate = { + roomType: roomType, + priceName: name, + public: publicPrice, + member: memberPrice, + } + handleSelectRate(rate) + } + return (
    ) diff --git a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css index a5176f517..66a27302e 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/roomSelection.module.css @@ -20,15 +20,6 @@ width: 0; } -.summary { - position: fixed; - bottom: 0; - left: 0; - right: 0; - background-color: white; - padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5); -} - @media (min-width: 767px) { .roomList { grid-template-columns: repeat(3, minmax(240px, 1fr)); diff --git a/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css b/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css index ae0d0e20e..7940ee17f 100644 --- a/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css +++ b/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.module.css @@ -6,13 +6,6 @@ padding: var(--Spacing-x1); } -.accordionItem.light { - background-color: var(--Base-Surface-Primary-light-Normal); -} -.accordionItem.subtle { - background-color: var(--Base-Background-Primary-Normal); -} - .summary { position: relative; display: flex; @@ -48,7 +41,7 @@ } .content { - padding: 0 var(--Spacing-x-one-and-half); + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half) var(--Spacing-x1); overflow: hidden; max-height: 0; transition: max-height 0.3s; diff --git a/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.ts b/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.ts index aebdba957..8f4ff07ca 100644 --- a/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.ts +++ b/components/TempDesignSystem/Accordion/AccordionItem/accordionItem.ts @@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority" import { accordionItemVariants } from "./variants" -import { IconName } from "@/types/components/icon" +import type { IconName } from "@/types/components/icon" export interface AccordionItemProps extends React.HtmlHTMLAttributes, diff --git a/components/TempDesignSystem/Accordion/AccordionItem/index.tsx b/components/TempDesignSystem/Accordion/AccordionItem/index.tsx index 84be2d010..013eb746d 100644 --- a/components/TempDesignSystem/Accordion/AccordionItem/index.tsx +++ b/components/TempDesignSystem/Accordion/AccordionItem/index.tsx @@ -5,17 +5,19 @@ import { useRef } from "react" import { ChevronDownIcon } from "@/components/Icons" import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" -import { AccordionItemProps } from "./accordionItem" import { accordionItemVariants } from "./variants" import styles from "./accordionItem.module.css" +import type { AccordionItemProps } from "./accordionItem" + export default function AccordionItem({ children, icon, title, theme, variant, + className, }: AccordionItemProps) { const contentRef = useRef(null) const detailsRef = useRef(null) @@ -43,7 +45,7 @@ export default function AccordionItem({ } return ( -
  • +
  • {IconComp && } diff --git a/components/TempDesignSystem/Accordion/accordion.module.css b/components/TempDesignSystem/Accordion/accordion.module.css index 0f9be355e..64f18730c 100644 --- a/components/TempDesignSystem/Accordion/accordion.module.css +++ b/components/TempDesignSystem/Accordion/accordion.module.css @@ -12,3 +12,7 @@ .accordion.subtle { background-color: var(--Base-Background-Primary-Normal); } + +.accordion li:last-child { + border: none; +} diff --git a/components/TempDesignSystem/Accordion/index.tsx b/components/TempDesignSystem/Accordion/index.tsx index 64afe99b9..822026e53 100644 --- a/components/TempDesignSystem/Accordion/index.tsx +++ b/components/TempDesignSystem/Accordion/index.tsx @@ -1,9 +1,10 @@ import { Children, cloneElement, isValidElement } from "react" import { AccordionItemProps } from "./AccordionItem/accordionItem" -import { AccordionProps } from "./accordion" import { accordionVariants } from "./variants" +import type { AccordionProps } from "./accordion" + export default function Accordion({ children, className, diff --git a/components/TempDesignSystem/Card/card.module.css b/components/TempDesignSystem/Card/card.module.css index 4f501d1cf..6dd063c76 100644 --- a/components/TempDesignSystem/Card/card.module.css +++ b/components/TempDesignSystem/Card/card.module.css @@ -60,7 +60,7 @@ } .themeThree { - --font-color: var(--Tertiary-Light-Surface-Text); + --font-color: var(--Tertiary-Light-On-Surface-Text); --script-color: var(--Tertiary-Light-On-Surface-Accent); background: var(--Tertiary-Light-Surface-Normal); @@ -74,7 +74,7 @@ } .themePrimaryDim { - --font-color: var(--Primary-Light-On-Surface-Text); + --font-color: var(--Primary-Dim-On-Surface-Text); --script-color: var(--Primary-Dim-On-Surface-Accent); background: var(--Primary-Dim-Surface-Normal); diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 7f7c8f04f..0afc51797 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -9,6 +9,11 @@ background: var(--UI-Input-Controls-Fill-Selected); } +.container[data-disabled] .checkbox { + border: 1px solid var(--UI-Input-Controls-Border-Disabled); + background: var(--UI-Input-Controls-Surface-Disabled); +} + .checkboxContainer { display: flex; align-items: flex-start; @@ -20,13 +25,12 @@ width: 24px; height: 24px; min-width: 24px; - border: 2px solid var(--UI-Input-Controls-Border-Normal); + border: 1px solid var(--UI-Input-Controls-Border-Normal); border-radius: 4px; transition: all 200ms; display: flex; align-items: center; justify-content: center; - transition: all 200ms; forced-color-adjust: none; } diff --git a/components/TempDesignSystem/Form/Checkbox/index.tsx b/components/TempDesignSystem/Form/Checkbox/index.tsx index cb4a68cde..ee3967d8d 100644 --- a/components/TempDesignSystem/Form/Checkbox/index.tsx +++ b/components/TempDesignSystem/Form/Checkbox/index.tsx @@ -7,10 +7,10 @@ import { InfoCircleIcon } from "@/components/Icons" import CheckIcon from "@/components/Icons/Check" import Caption from "@/components/TempDesignSystem/Text/Caption" -import { CheckboxProps } from "./checkbox" - import styles from "./checkbox.module.css" +import { CheckboxProps } from "@/types/components/checkbox" + export default function Checkbox({ name, children, @@ -29,6 +29,7 @@ export default function Checkbox({ isSelected={field.value} onChange={field.onChange} data-testid={name} + isDisabled={registerOptions?.disabled} > {({ isSelected }) => ( <> diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index 5de39b49e..501d43449 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -29,14 +29,14 @@ export default function Card({ return (
  • ) @@ -148,9 +152,10 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="MM" required tabIndex={2} - defaultValue={ + defaultSelectedKey={ segment.isPlaceholder ? undefined : segment.value } + value={segment.isPlaceholder ? undefined : segment.value} />
    ) @@ -166,9 +171,10 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="YYYY" required tabIndex={1} - defaultValue={ + defaultSelectedKey={ segment.isPlaceholder ? undefined : segment.value } + value={segment.isPlaceholder ? undefined : segment.value} /> ) diff --git a/components/TempDesignSystem/Form/NewPassword/index.tsx b/components/TempDesignSystem/Form/NewPassword/index.tsx index 070a282af..2c0a6d31a 100644 --- a/components/TempDesignSystem/Form/NewPassword/index.tsx +++ b/components/TempDesignSystem/Form/NewPassword/index.tsx @@ -30,10 +30,11 @@ export default function NewPassword({ placeholder = "", registerOptions = {}, label, + visibilityToggleable = true, }: NewPasswordProps) { const { control } = useFormContext() const intl = useIntl() - const [isPasswordVisible, setPasswordVisible] = useState(false) + const [isPasswordVisible, setIsPasswordVisible] = useState(false) function getErrorMessage(key: PasswordValidatorKey) { switch (key) { @@ -69,7 +70,9 @@ export default function NewPassword({ onChange={field.onChange} validationBehavior="aria" value={field.value} - type={isPasswordVisible ? "text" : "password"} + type={ + visibilityToggleable && isPasswordVisible ? "text" : "password" + } >
    - + {visibilityToggleable ? ( + + ) : null}
    {field.value ? (
    diff --git a/components/TempDesignSystem/Form/NewPassword/newPassword.ts b/components/TempDesignSystem/Form/NewPassword/newPassword.ts index e6835cb58..8486a44ad 100644 --- a/components/TempDesignSystem/Form/NewPassword/newPassword.ts +++ b/components/TempDesignSystem/Form/NewPassword/newPassword.ts @@ -4,6 +4,7 @@ export interface NewPasswordProps extends React.InputHTMLAttributes { label?: string registerOptions?: RegisterOptions + visibilityToggleable?: boolean } export interface IconProps { diff --git a/components/TempDesignSystem/Grids/Stackable/stackable.module.css b/components/TempDesignSystem/Grids/Stackable/stackable.module.css index 671e0fa6d..c7eb1af94 100644 --- a/components/TempDesignSystem/Grids/Stackable/stackable.module.css +++ b/components/TempDesignSystem/Grids/Stackable/stackable.module.css @@ -16,4 +16,8 @@ .twoColumns { grid-template-columns: repeat(2, 1fr); } + + .oneColumn { + grid-template-columns: 1fr; + } } diff --git a/components/TempDesignSystem/Grids/Stackable/stackable.ts b/components/TempDesignSystem/Grids/Stackable/stackable.ts index 0f5c249f7..293197e26 100644 --- a/components/TempDesignSystem/Grids/Stackable/stackable.ts +++ b/components/TempDesignSystem/Grids/Stackable/stackable.ts @@ -4,6 +4,4 @@ import type { VariantProps } from "class-variance-authority" export interface StackableGridProps extends React.HTMLAttributes, - VariantProps { - columns?: 2 | 3 -} + VariantProps {} diff --git a/components/TempDesignSystem/Grids/Stackable/variants.ts b/components/TempDesignSystem/Grids/Stackable/variants.ts index fbe93cd7c..e69a0e699 100644 --- a/components/TempDesignSystem/Grids/Stackable/variants.ts +++ b/components/TempDesignSystem/Grids/Stackable/variants.ts @@ -5,6 +5,7 @@ import styles from "./stackable.module.css" export const stackableGridVariants = cva(styles.container, { variants: { columns: { + 1: styles.oneColumn, 2: styles.twoColumns, 3: styles.threeColumns, }, diff --git a/components/TempDesignSystem/LinkChips/Chip/index.tsx b/components/TempDesignSystem/LinkChips/Chip/index.tsx index 6ec804629..d32dc1a53 100644 --- a/components/TempDesignSystem/LinkChips/Chip/index.tsx +++ b/components/TempDesignSystem/LinkChips/Chip/index.tsx @@ -9,7 +9,7 @@ import type { LinkChipProps } from "./chip" export default function LinkChip({ url, title }: LinkChipProps) { return ( - + {title} diff --git a/components/TempDesignSystem/ShowMoreButton/index.tsx b/components/TempDesignSystem/ShowMoreButton/index.tsx index ca8a24dcb..7be723fd1 100644 --- a/components/TempDesignSystem/ShowMoreButton/index.tsx +++ b/components/TempDesignSystem/ShowMoreButton/index.tsx @@ -16,6 +16,8 @@ export default function ShowMoreButton({ intent, disabled, showLess, + textShowMore = "Show less", + textShowLess = "Show more", loadMoreData, }: ShowMoreButtonProps) { const intl = useIntl() @@ -36,7 +38,7 @@ export default function ShowMoreButton({ intent="text" > - {intl.formatMessage({ id: showLess ? "Show less" : "Show more" })} + {intl.formatMessage({ id: showLess ? textShowLess : textShowMore })}
    ) diff --git a/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts b/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts index b6ad929f5..1b7d66b07 100644 --- a/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts +++ b/components/TempDesignSystem/ShowMoreButton/showMoreButton.ts @@ -7,5 +7,7 @@ export interface ShowMoreButtonProps VariantProps { disabled?: boolean showLess?: boolean + textShowMore?: string + textShowLess?: string loadMoreData: () => void } diff --git a/components/TempDesignSystem/SidePeek/index.tsx b/components/TempDesignSystem/SidePeek/index.tsx index ee59c0e28..708404d15 100644 --- a/components/TempDesignSystem/SidePeek/index.tsx +++ b/components/TempDesignSystem/SidePeek/index.tsx @@ -2,12 +2,7 @@ import { useIsSSR } from "@react-aria/ssr" import { useContext, useState } from "react" -import { - Dialog, - DialogTrigger, - Modal, - ModalOverlay, -} from "react-aria-components" +import { Dialog, Modal, ModalOverlay } from "react-aria-components" import { useIntl } from "react-intl" import { CloseLargeIcon } from "@/components/Icons" @@ -52,48 +47,45 @@ function SidePeek({ ) } - return (
    - - - - - + + +
    ) } diff --git a/components/TempDesignSystem/Text/Body/body.module.css b/components/TempDesignSystem/Text/Body/body.module.css index 4243eb690..dd9b85156 100644 --- a/components/TempDesignSystem/Text/Body/body.module.css +++ b/components/TempDesignSystem/Text/Body/body.module.css @@ -96,6 +96,10 @@ color: var(--UI-Text-High-contrast); } +.uiTextMediumContrast { + color: var(--UI-Text-Medium-contrast); +} + .uiTextPlaceholder { color: var(--UI-Text-Placeholder); } diff --git a/components/TempDesignSystem/Text/Body/variants.ts b/components/TempDesignSystem/Text/Body/variants.ts index 5c23fe506..1663e21f0 100644 --- a/components/TempDesignSystem/Text/Body/variants.ts +++ b/components/TempDesignSystem/Text/Body/variants.ts @@ -17,6 +17,7 @@ const config = { peach50: styles.peach50, peach80: styles.peach80, uiTextHighContrast: styles.uiTextHighContrast, + uiTextMediumContrast: styles.uiTextMediumContrast, uiTextPlaceholder: styles.uiTextPlaceholder, }, textAlign: { diff --git a/components/TempDesignSystem/Text/Caption/caption.module.css b/components/TempDesignSystem/Text/Caption/caption.module.css index b5b9192bc..bbd4a037d 100644 --- a/components/TempDesignSystem/Text/Caption/caption.module.css +++ b/components/TempDesignSystem/Text/Caption/caption.module.css @@ -10,20 +10,19 @@ p.caption { .bold { font-family: var(--typography-Caption-Bold-fontFamily); font-size: var(--typography-Caption-Bold-fontSize); - font-weight: var(--typography-Caption-Bold-fontWeight); + font-weight: 500; /* var(--typography-Caption-Bold-fontWeight); /* Commented till figma values are fixed to 500 instead of medium */ letter-spacing: var(--typography-Caption-Bold-letterSpacing); line-height: var(--typography-Caption-Bold-lineHeight); text-decoration: var(--typography-Caption-Bold-textDecoration); } -.uppercase { - font-family: var(--typography-Caption-Bold-fontFamily); - font-size: var(--typography-Caption-Bold-fontSize); - font-weight: var(--typography-Caption-Bold-fontWeight); - letter-spacing: var(--typography-Caption-Bold-letterSpacing); - line-height: var(--typography-Caption-Bold-lineHeight); - text-decoration: var(--typography-Caption-Bold-textDecoration); - text-transform: uppercase; +.labels { + font-family: var(--typography-Caption-Labels-fontFamily); + font-size: var(--typography-Caption-Labels-fontSize); + font-weight: var(--typography-Caption-Labels-fontWeight); + letter-spacing: var(--typography-Caption-Labels-letterSpacing); + line-height: var(--typography-Caption-Labels-lineHeight); + text-decoration: var(--typography-Caption-Labels-textDecoration); } .regular { @@ -35,6 +34,10 @@ p.caption { text-decoration: var(--typography-Caption-Regular-textDecoration); } +.uppercase { + text-transform: uppercase; +} + .baseTextAccent { color: var(--Base-Text-Accent); } diff --git a/components/TempDesignSystem/Text/Caption/index.tsx b/components/TempDesignSystem/Text/Caption/index.tsx index e80caf79b..357082dd2 100644 --- a/components/TempDesignSystem/Text/Caption/index.tsx +++ b/components/TempDesignSystem/Text/Caption/index.tsx @@ -12,6 +12,7 @@ export default function Caption({ textAlign, textTransform, uppercase, + type, ...props }: CaptionProps) { const Comp = asChild ? Slot : "p" @@ -20,6 +21,7 @@ export default function Caption({ className, textTransform, uppercase, + type, }) : captionVariants({ className, @@ -27,6 +29,7 @@ export default function Caption({ textTransform, textAlign, uppercase, + type, }) return } diff --git a/components/TempDesignSystem/Text/Caption/variants.ts b/components/TempDesignSystem/Text/Caption/variants.ts index 52d8d0c9f..6fe5b4dde 100644 --- a/components/TempDesignSystem/Text/Caption/variants.ts +++ b/components/TempDesignSystem/Text/Caption/variants.ts @@ -4,6 +4,11 @@ import styles from "./caption.module.css" const config = { variants: { + type: { + regular: styles.regular, + bold: styles.bold, + label: styles.labels, + }, color: { baseTextAccent: styles.baseTextAccent, black: styles.black, @@ -19,8 +24,6 @@ const config = { disabled: styles.disabled, }, textTransform: { - bold: styles.bold, - regular: styles.regular, uppercase: styles.uppercase, }, textAlign: { @@ -33,7 +36,7 @@ const config = { }, defaultVariants: { color: "black", - textTransform: "regular", + type: "regular", }, } as const @@ -41,9 +44,12 @@ export const captionVariants = cva(styles.caption, config) const fontOnlyConfig = { variants: { - textTransform: { - bold: styles.bold, + type: { regular: styles.regular, + bold: styles.bold, + label: styles.labels, + }, + textTransform: { uppercase: styles.uppercase, }, uppercase: { @@ -51,7 +57,7 @@ const fontOnlyConfig = { }, }, defaultVariants: { - textTransform: "regular", + type: "regular", }, } as const diff --git a/components/TempDesignSystem/Text/Footnote/footnote.module.css b/components/TempDesignSystem/Text/Footnote/footnote.module.css index 4c2e36b7f..e1b1d550d 100644 --- a/components/TempDesignSystem/Text/Footnote/footnote.module.css +++ b/components/TempDesignSystem/Text/Footnote/footnote.module.css @@ -25,13 +25,16 @@ text-decoration: var(--typography-Footnote-Regular-textDecoration); } +.labels { + font-family: var(--typography-Footnote-Labels-fontFamily); + font-size: var(--typography-Footnote-Labels-fontSize); + font-weight: var(--typography-Footnote-Labels-fontWeight); + letter-spacing: var(--typography-Footnote-Labels-letterSpacing); + line-height: var(--typography-Footnote-Labels-lineHeight); + text-decoration: var(--typography-Footnote-Labels-textDecoration); +} + .uppercase { - font-family: var(--typography-Footnote-Regular-fontFamily); - font-size: var(--typography-Footnote-Regular-fontSize); - font-weight: var(--typography-Footnote-Bold-fontWeight); - letter-spacing: var(--typography-Footnote-Regular-letterSpacing); - line-height: var(--typography-Footnote-Regular-lineHeight); - text-decoration: var(--typography-Footnote-Regular-textDecoration); text-transform: uppercase; } diff --git a/components/TempDesignSystem/Text/Footnote/index.tsx b/components/TempDesignSystem/Text/Footnote/index.tsx index b8c834672..bc34d2471 100644 --- a/components/TempDesignSystem/Text/Footnote/index.tsx +++ b/components/TempDesignSystem/Text/Footnote/index.tsx @@ -11,20 +11,23 @@ export default function Footnote({ fontOnly = false, textAlign, textTransform, + type, ...props }: FootnoteProps) { const Comp = asChild ? Slot : "p" const classNames = fontOnly ? footnoteFontOnlyVariants({ - className, - textAlign, - textTransform, - }) + className, + textAlign, + textTransform, + type, + }) : footnoteVariants({ - className, - color, - textAlign, - textTransform, - }) + className, + color, + textAlign, + textTransform, + type, + }) return } diff --git a/components/TempDesignSystem/Text/Footnote/variants.ts b/components/TempDesignSystem/Text/Footnote/variants.ts index 1d4ad921e..311d81a58 100644 --- a/components/TempDesignSystem/Text/Footnote/variants.ts +++ b/components/TempDesignSystem/Text/Footnote/variants.ts @@ -4,6 +4,11 @@ import styles from "./footnote.module.css" const config = { variants: { + type: { + regular: styles.regular, + bold: styles.bold, + label: styles.labels, + }, color: { black: styles.black, burgundy: styles.burgundy, @@ -18,13 +23,11 @@ const config = { left: styles.left, }, textTransform: { - bold: styles.bold, - regular: styles.regular, uppercase: styles.uppercase, }, }, defaultVariants: { - textTransform: "regular", + type: "regular", }, } as const @@ -32,18 +35,21 @@ export const footnoteVariants = cva(styles.footnote, config) const fontOnlyConfig = { variants: { + type: { + regular: styles.regular, + bold: styles.bold, + label: styles.labels, + }, textAlign: { center: styles.center, left: styles.left, }, textTransform: { - bold: styles.bold, - regular: styles.regular, uppercase: styles.uppercase, }, }, defaultVariants: { - textTransform: "regular", + type: "regular", }, } as const diff --git a/components/TempDesignSystem/Tooltip/index.tsx b/components/TempDesignSystem/Tooltip/index.tsx index ba53c022d..ce133b5ee 100644 --- a/components/TempDesignSystem/Tooltip/index.tsx +++ b/components/TempDesignSystem/Tooltip/index.tsx @@ -20,7 +20,7 @@ export function Tooltip

    ({

    {heading && ( - + {heading} )} diff --git a/env/server.ts b/env/server.ts index 0345ee457..73433f811 100644 --- a/env/server.ts +++ b/env/server.ts @@ -44,15 +44,15 @@ export const env = createEnv({ .default("false"), NEXTAUTH_REDIRECT_PROXY_URL: z.string().optional(), NEXTAUTH_SECRET: z.string(), - NEXTAUTH_URL: z.string().optional(), - AUTH_URL: z.string().optional(), + NEXTAUTH_URL: z.string().default(""), + AUTH_URL: z.string().default(""), NODE_ENV: z.enum(["development", "test", "production"]), PRINT_QUERY: z .string() .refine((s) => s === "true" || s === "false") .transform((s) => s === "true") .default("false"), - PUBLIC_URL: z.string().optional(), + PUBLIC_URL: z.string().default(""), REVALIDATE_SECRET: z.string(), SALESFORCE_PREFERENCE_BASE_URL: z.string(), SEAMLESS_LOGIN_DA: z.string(), diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index bbb2b33f6..24675f021 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -4,15 +4,18 @@ "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A photo of the room": "Et foto af værelset", "About meetings & conferences": "About meetings & conferences", + "About the hotel": "About the hotel", "Activities": "Aktiviteter", "Add code": "Tilføj kode", "Add new card": "Tilføj nyt kort", "Address": "Adresse", "Airport": "Lufthavn", + "Adults": "voksne", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", "Amusement park": "Forlystelsespark", + "An error occurred. Please try again.": "Der opstod en fejl. Prøv venligst igen.", "An error occurred trying to manage your preferences, please try again later.": "Der opstod en fejl under forsøget på at administrere dine præferencer. Prøv venligst igen senere.", "An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.", "An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.", @@ -44,6 +47,7 @@ "Check in": "Check ind", "Check out": "Check ud", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.", + "Children": "børn", "Choose room": "Vælg rum", "Cities": "Byer", "City": "By", @@ -82,7 +86,9 @@ "Discard unsaved changes?": "Slette ændringer, der ikke er gemt?", "Distance to city centre": "{number}km til centrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte morgenbuffet?", + "Done": "Færdig", "Download the Scandic app": "Download Scandic-appen", + "Driving directions": "Kørselsanvisning", "Earn bonus nights & points": "Optjen bonusnætter og point", "Edit": "Redigere", "Edit profile": "Rediger profil", @@ -100,8 +106,9 @@ "Fair": "Messe", "Find booking": "Find booking", "Find hotels": "Find hotel", - "Firstname": "Fornavn", + "First name": "Fornavn", "Flexibility": "Fleksibilitet", + "Follow us": "Følg os", "Former Scandic Hotel": "Tidligere Scandic Hotel", "Free cancellation": "Gratis afbestilling", "Free rebooking": "Gratis ombooking", @@ -123,13 +130,16 @@ "How it works": "Hvordan det virker", "I would like to get my booking confirmation via sms": "Jeg vil gerne få min booking bekræftelse via SMS", "Image gallery": "Billedgalleri", + "In adults bed": "i de voksnes seng", + "In crib": "i tremmeseng", + "In extra bed": "i ekstra seng", "Included": "Inkluderet", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", "King bed": "Kingsize-seng", "Language": "Sprog", - "Lastname": "Efternavn", + "Last name": "Efternavn", "Latest searches": "Seneste søgninger", "Left": "tilbage", "Level": "Niveau", @@ -182,6 +192,7 @@ "No, keep card": "Nej, behold kortet", "Non refundable": "Ikke-refunderbart", "Non-refundable": "Ikke-refunderbart", + "Nordic Swan Ecolabel": "Svanemærket", "Not found": "Ikke fundet", "Nr night, nr adult": "{nights, number} nat, {adults, number} voksen", "On your journey": "På din rejse", @@ -224,6 +235,7 @@ "Retype new password": "Gentag den nye adgangskode", "Room & Terms": "Værelse & Vilkår", "Room facilities": "Værelsesfaciliteter", + "Room": "Værelse", "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sauna and gym": "Sauna and gym", @@ -232,8 +244,10 @@ "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", "See hotel details": "Se hoteloplysninger", + "See less FAQ": "Se mindre FAQ", "See room details": "Se værelsesdetaljer", "See rooms": "Se værelser", "Select a country": "Vælg et land", @@ -248,8 +262,10 @@ "Shopping & Dining": "Shopping & Spisning", "Show all amenities": "Vis alle faciliteter", "Show less": "Vis mindre", + "Show less rooms": "Vise færre rum", "Show map": "Vis kort", "Show more": "Vis mere", + "Show more rooms": "Vise flere rum", "Sign up bonus": "Velkomstbonus", "Sign up to Scandic Friends": "Tilmeld dig Scandic Friends", "Skip to main content": "Spring over og gå til hovedindhold", @@ -319,6 +335,7 @@ "Zoom out": "Zoom ud", "as of today": "pr. dags dato", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", + "booking.children": "{totalChildren, plural, one {# barn} other {# børn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gæst} other {# gæster}}", "booking.nights": "{totalNights, plural, one {# nat} other {# nætter}}", "booking.rooms": "{totalRooms, plural, one {# værelse} other {# værelser}}", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 5798fd0a9..785c93f89 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -4,15 +4,18 @@ "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A photo of the room": "Ein Foto des Zimmers", "About meetings & conferences": "About meetings & conferences", + "About the hotel": "Über das Hotel", "Activities": "Aktivitäten", "Add code": "Code hinzufügen", "Add new card": "Neue Karte hinzufügen", "Address": "Adresse", "Airport": "Flughafen", + "Adults": "Erwachsene", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", "Amusement park": "Vergnügungspark", + "An error occurred. Please try again.": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", "An error occurred trying to manage your preferences, please try again later.": "Beim Versuch, Ihre Einstellungen zu verwalten, ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", "An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", "An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.", @@ -44,6 +47,7 @@ "Check in": "Einchecken", "Check out": "Auschecken", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.", + "Children": "Kinder", "Choose room": "Zimmer wählen", "Cities": "Städte", "City": "Stadt", @@ -82,7 +86,9 @@ "Discard unsaved changes?": "Nicht gespeicherte Änderungen verwerfen?", "Distance to city centre": "{number}km zum Stadtzentrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Möchten Sie den Tag mit Scandics berühmtem Frühstücksbuffet beginnen?", + "Done": "Fertig", "Download the Scandic app": "Laden Sie die Scandic-App herunter", + "Driving directions": "Anfahrtsbeschreibung", "Earn bonus nights & points": "Sammeln Sie Bonusnächte und -punkte", "Edit": "Bearbeiten", "Edit profile": "Profil bearbeiten", @@ -100,8 +106,9 @@ "Fair": "Messe", "Find booking": "Buchung finden", "Find hotels": "Hotels finden", - "Firstname": "Vorname", + "First name": "Vorname", "Flexibility": "Flexibilität", + "Follow us": "Folgen Sie uns", "Former Scandic Hotel": "Ehemaliges Scandic Hotel", "Free cancellation": "Kostenlose Stornierung", "Free rebooking": "Kostenlose Umbuchung", @@ -123,13 +130,16 @@ "How it works": "Wie es funktioniert", "I would like to get my booking confirmation via sms": "Ich möchte meine Buchungsbestätigung per SMS erhalten", "Image gallery": "Bildergalerie", + "In adults bed": "Im Bett der Eltern", + "In crib": "im Kinderbett", + "In extra bed": "im zusätzlichen Bett", "Included": "Iinklusive", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "Join Scandic Friends": "Treten Sie Scandic Friends bei", "Join at no cost": "Kostenlos beitreten", "King bed": "Kingsize-Bett", "Language": "Sprache", - "Lastname": "Nachname", + "Last name": "Nachname", "Latest searches": "Letzte Suchanfragen", "Left": "übrig", "Level": "Level", @@ -182,6 +192,7 @@ "No, keep card": "Nein, Karte behalten", "Non refundable": "Nicht erstattungsfähig", "Non-refundable": "Nicht erstattungsfähig", + "Nordic Swan Ecolabel": "Nordic Swan Ecolabel", "Not found": "Nicht gefunden", "Nr night, nr adult": "{nights, number} Nacht, {adults, number} Erwachsener", "On your journey": "Auf deiner Reise", @@ -225,6 +236,7 @@ "Room & Terms": "Zimmer & Bedingungen", "Room facilities": "Zimmerausstattung", "Rooms": "Räume", + "Room": "Zimmer", "Rooms & Guests": "Zimmer & Gäste", "Sauna and gym": "Sauna and gym", "Save": "Speichern", @@ -232,8 +244,10 @@ "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", "See hotel details": "Hotelinformationen ansehen", + "See less FAQ": "Weniger anzeigen FAQ", "See room details": "Zimmerdetails ansehen", "See rooms": "Zimmer ansehen", "Select a country": "Wähle ein Land", @@ -248,8 +262,10 @@ "Shopping & Dining": "Einkaufen & Essen", "Show all amenities": "Alle Annehmlichkeiten anzeigen", "Show less": "Weniger anzeigen", + "Show less rooms": "Weniger Zimmer anzeigen", "Show map": "Karte anzeigen", "Show more": "Mehr anzeigen", + "Show more rooms": "Weitere Räume anzeigen", "Sign up bonus": "Anmelde-Bonus", "Sign up to Scandic Friends": "Treten Sie Scandic Friends bei", "Skip to main content": "Direkt zum Inhalt", @@ -319,6 +335,7 @@ "Zoom out": "Verkleinern", "as of today": "Stand heute", "booking.adults": "{totalAdults, plural, one {# erwachsene} other {# erwachsene}}", + "booking.children": "{totalChildren, plural, one {# kind} other {# kinder}}", "booking.guests": "Max {nrOfGuests, plural, one {# gast} other {# gäste}}", "booking.nights": "{totalNights, plural, one {# nacht} other {# Nächte}}", "booking.rooms": "{totalRooms, plural, one {# zimmer} other {# räume}}", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 39c9364cd..a05170e00 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -4,15 +4,19 @@ "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A photo of the room": "A photo of the room", "About meetings & conferences": "About meetings & conferences", + "About the hotel": "About the hotel", "Activities": "Activities", "Add code": "Add code", "Add new card": "Add new card", "Address": "Address", "Airport": "Airport", + "Adults": "Adults", + "Age": "Age", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", "Already a friend?": "Already a friend?", "Amenities": "Amenities", "Amusement park": "Amusement park", + "An error occurred. Please try again.": "An error occurred. Please try again.", "An error occurred trying to manage your preferences, please try again later.": "An error occurred trying to manage your preferences, please try again later.", "An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.", "An error occurred when trying to update profile.": "An error occurred when trying to update profile.", @@ -27,6 +31,7 @@ "Attractions": "Attractions", "Back to scandichotels.com": "Back to scandichotels.com", "Bar": "Bar", + "Bed": "Bed", "Bed type": "Bed type", "Birth date": "Birth date", "Book": "Book", @@ -44,6 +49,8 @@ "Check in": "Check in", "Check out": "Check out", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", + "Children": "Children", + "Child age is required": "Child age is required", "Choose room": "Choose room", "Cities": "Cities", "City": "City", @@ -82,7 +89,9 @@ "Discard unsaved changes?": "Discard unsaved changes?", "Distance to city centre": "{number}km to city centre", "Do you want to start the day with Scandics famous breakfast buffé?": "Do you want to start the day with Scandics famous breakfast buffé?", + "Done": "Done", "Download the Scandic app": "Download the Scandic app", + "Driving directions": "Driving directions", "Earn bonus nights & points": "Earn bonus nights & points", "Edit": "Edit", "Edit profile": "Edit profile", @@ -100,8 +109,9 @@ "Fair": "Fair", "Find booking": "Find booking", "Find hotels": "Find hotels", - "Firstname": "Firstname", + "First name": "First name", "Flexibility": "Flexibility", + "Follow us": "Follow us", "Former Scandic Hotel": "Former Scandic Hotel", "Free cancellation": "Free cancellation", "Free rebooking": "Free rebooking", @@ -123,13 +133,16 @@ "How it works": "How it works", "I would like to get my booking confirmation via sms": "I would like to get my booking confirmation via sms", "Image gallery": "Image gallery", + "In adults bed": "In adults bed", + "In crib": "In crib", + "In extra bed": "In extra bed", "Included": "Included", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", "King bed": "King bed", "Language": "Language", - "Lastname": "Lastname", + "Last name": "Last name", "Latest searches": "Latest searches", "Left": "left", "Level": "Level", @@ -182,6 +195,7 @@ "No, keep card": "No, keep card", "Non refundable": "Non refundable", "Non-refundable": "Non-refundable", + "Nordic Swan Ecolabel": "Nordic Swan Ecolabel", "Not found": "Not found", "Nr night, nr adult": "{nights, number} night, {adults, number} adult", "On your journey": "On your journey", @@ -224,6 +238,7 @@ "Retype new password": "Retype new password", "Room & Terms": "Room & Terms", "Room facilities": "Room facilities", + "Room": "Room", "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sauna and gym": "Sauna and gym", @@ -232,8 +247,10 @@ "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", "See hotel details": "See hotel details", + "See less FAQ": "See less FAQ", "See room details": "See room details", "See rooms": "See rooms", "Select a country": "Select a country", @@ -248,8 +265,10 @@ "Shopping & Dining": "Shopping & Dining", "Show all amenities": "Show all amenities", "Show less": "Show less", + "Show less rooms": "Show less rooms", "Show map": "Show map", "Show more": "Show more", + "Show more rooms": "Show more rooms", "Sign up bonus": "Sign up bonus", "Sign up to Scandic Friends": "Sign up to Scandic Friends", "Skip to main content": "Skip to main content", @@ -319,6 +338,7 @@ "Zoom out": "Zoom out", "as of today": "as of today", "booking.adults": "{totalAdults, plural, one {# adult} other {# adults}}", + "booking.children": "{totalChildren, plural, one {# child} other {# children}}", "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 24740746a..1c2b11a26 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -4,15 +4,18 @@ "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A photo of the room": "Kuva huoneesta", "About meetings & conferences": "About meetings & conferences", + "About the hotel": "Tietoja hotellista", "Activities": "Aktiviteetit", "Add code": "Lisää koodi", "Add new card": "Lisää uusi kortti", "Address": "Osoite", "Airport": "Lentokenttä", + "Adults": "Aikuista", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", "Amusement park": "Huvipuisto", + "An error occurred. Please try again.": "Tapahtui virhe. Yritä uudelleen.", "An error occurred trying to manage your preferences, please try again later.": "Asetusten hallinnassa tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", "An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.", @@ -44,6 +47,7 @@ "Check in": "Sisäänkirjautuminen", "Check out": "Uloskirjautuminen", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.", + "Children": "Lasta", "Choose room": "Valitse huone", "Cities": "Kaupungit", "City": "Kaupunki", @@ -82,7 +86,9 @@ "Discard unsaved changes?": "Hylkäätkö tallentamattomat muutokset?", "Distance to city centre": "{number}km Etäisyys kaupunkiin", "Do you want to start the day with Scandics famous breakfast buffé?": "Haluatko aloittaa päiväsi Scandicsin kuuluisalla aamiaisbuffella?", + "Done": "Valmis", "Download the Scandic app": "Lataa Scandic-sovellus", + "Driving directions": "Ajo-ohjeet", "Earn bonus nights & points": "Ansaitse bonusöitä ja -pisteitä", "Edit": "Muokata", "Edit profile": "Muokkaa profiilia", @@ -100,8 +106,9 @@ "Fair": "Messukeskus", "Find booking": "Etsi varaus", "Find hotels": "Etsi hotelleja", - "Firstname": "Etunimi", + "First name": "Etunimi", "Flexibility": "Joustavuus", + "Follow us": "Seuraa meitä", "Former Scandic Hotel": "Entinen Scandic-hotelli", "Free cancellation": "Ilmainen peruutus", "Free rebooking": "Ilmainen uudelleenvaraus", @@ -123,13 +130,16 @@ "How it works": "Kuinka se toimii", "I would like to get my booking confirmation via sms": "Haluan saada varauksen vahvistuksen SMS-viestillä", "Image gallery": "Kuvagalleria", + "In adults bed": "Aikuisten vuoteessa", + "In crib": "Pinnasängyssä", + "In extra bed": "Oma vuodepaikka", "Included": "Sisälly hintaan", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", "King bed": "King-vuode", "Language": "Kieli", - "Lastname": "Sukunimi", + "Last name": "Sukunimi", "Latest searches": "Viimeisimmät haut", "Left": "jäljellä", "Level": "Level", @@ -182,6 +192,7 @@ "No, keep card": "Ei, pidä kortti", "Non refundable": "Ei palautettavissa", "Non-refundable": "Ei palautettavissa", + "Nordic Swan Ecolabel": "Ympäristömerkki Miljömärkt", "Not found": "Ei löydetty", "Nr night, nr adult": "{nights, number} yö, {adults, number} aikuinen", "On your journey": "Matkallasi", @@ -224,6 +235,7 @@ "Retype new password": "Kirjoita uusi salasana uudelleen", "Room & Terms": "Huone & Ehdot", "Room facilities": "Huoneen varustelu", + "Room": "Huone", "Rooms": "Huoneet", "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", @@ -233,8 +245,10 @@ "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", "See hotel details": "Katso hotellin tiedot", + "See less FAQ": "Katso vähemmän UKK", "See room details": "Katso huoneen tiedot", "See rooms": "Katso huoneet", "Select a country": "Valitse maa", @@ -249,8 +263,10 @@ "Shopping & Dining": "Ostokset & Ravintolat", "Show all amenities": "Näytä kaikki mukavuudet", "Show less": "Näytä vähemmän", + "Show less rooms": "Näytä vähemmän huoneita", "Show map": "Näytä kartta", "Show more": "Näytä lisää", + "Show more rooms": "Näytä lisää huoneita", "Sign up bonus": "Liittymisbonus", "Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi", "Skip to main content": "Siirry pääsisältöön", @@ -320,6 +336,7 @@ "Zoom out": "Loitonna", "as of today": "tänään", "booking.adults": "{totalAdults, plural, one {# aikuinen} other {# aikuiset}}", + "booking.children": "{totalChildren, plural, one {# lapsi} other {# lasta}}", "booking.guests": "Max {nrOfGuests, plural, one {# vieras} other {# vieraita}}", "booking.nights": "{totalNights, plural, one {# yö} other {# yötä}}", "booking.rooms": "{totalRooms, plural, one {# huone} other {# sviitti}}", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 77e114db5..eafdab517 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -4,15 +4,18 @@ "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A photo of the room": "Et bilde av rommet", "About meetings & conferences": "About meetings & conferences", + "About the hotel": "Om hotellet", "Activities": "Aktiviteter", "Add code": "Legg til kode", "Add new card": "Legg til nytt kort", "Address": "Adresse", "Airport": "Flyplass", + "Adults": "Voksne", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", "Amusement park": "Tivoli", + "An error occurred. Please try again.": "Det oppsto en feil. Vennligst prøv igjen.", "An error occurred trying to manage your preferences, please try again later.": "Det oppstod en feil under forsøket på å administrere innstillingene dine. Prøv igjen senere.", "An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.", "An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.", @@ -44,6 +47,7 @@ "Check in": "Sjekk inn", "Check out": "Sjekk ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.", + "Children": "Barn", "Choose room": "Velg rom", "Cities": "Byer", "City": "By", @@ -81,7 +85,9 @@ "Discard unsaved changes?": "Forkaste endringer som ikke er lagret?", "Distance to city centre": "{number}km til sentrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Vil du starte dagen med Scandics berømte frokostbuffé?", + "Done": "Ferdig", "Download the Scandic app": "Last ned Scandic-appen", + "Driving directions": "Veibeskrivelser", "Earn bonus nights & points": "Tjen bonusnetter og poeng", "Edit": "Redigere", "Edit profile": "Rediger profil", @@ -99,8 +105,9 @@ "Fair": "Messe", "Find booking": "Finn booking", "Find hotels": "Finn hotell", - "Firstname": "Fornavn", + "First name": "Fornavn", "Flexibility": "Fleksibilitet", + "Follow us": "Følg oss", "Former Scandic Hotel": "Tidligere Scandic-hotell", "Free cancellation": "Gratis avbestilling", "Free rebooking": "Gratis ombooking", @@ -121,13 +128,16 @@ "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", "Image gallery": "Bildegalleri", + "In adults bed": "i voksnes seng", + "In crib": "i sprinkelseng", + "In extra bed": "i ekstraseng", "Included": "Inkludert", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", "King bed": "King-size-seng", "Language": "Språk", - "Lastname": "Etternavn", + "Last name": "Etternavn", "Latest searches": "Siste søk", "Left": "igjen", "Level": "Nivå", @@ -180,6 +190,7 @@ "No, keep card": "Nei, behold kortet", "Non refundable": "Ikke-refunderbart", "Non-refundable": "Ikke-refunderbart", + "Nordic Swan Ecolabel": "Svanemerket", "Not found": "Ikke funnet", "Nr night, nr adult": "{nights, number} natt, {adults, number} voksen", "On your journey": "På reisen din", @@ -222,6 +233,7 @@ "Retype new password": "Skriv inn nytt passord på nytt", "Room & Terms": "Rom & Vilkår", "Room facilities": "Romfasiliteter", + "Room": "Rom", "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sauna and gym": "Sauna and gym", @@ -230,8 +242,10 @@ "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", "See hotel details": "Se hotellinformasjon", + "See less FAQ": "Se mindre FAQ", "See room details": "Se detaljer om rommet", "See rooms": "Se rom", "Select a country": "Velg et land", @@ -246,8 +260,10 @@ "Shopping & Dining": "Shopping & Spisesteder", "Show all amenities": "Vis alle fasiliteter", "Show less": "Vis mindre", + "Show less rooms": "Vise færre rom", "Show map": "Vis kart", "Show more": "Vis mer", + "Show more rooms": "Vise flere rom", "Sign up bonus": "Velkomstbonus", "Sign up to Scandic Friends": "Bli med i Scandic Friends", "Skip to main content": "Gå videre til hovedsiden", @@ -317,6 +333,7 @@ "Zoom out": "Zoom ut", "as of today": "per i dag", "booking.adults": "{totalAdults, plural, one {# voksen} other {# voksne}}", + "booking.children": "{totalChildren, plural, one {# barn} other {# barn}}", "booking.guests": "Maks {nrOfGuests, plural, one {# gjest} other {# gjester}}", "booking.nights": "{totalNights, plural, one {# natt} other {# netter}}", "booking.rooms": "{totalRooms, plural, one {# rom} other {# rom}}", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 21ae78f1a..9eda58171 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -4,15 +4,18 @@ "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A photo of the room": "Ett foto av rummet", "About meetings & conferences": "About meetings & conferences", + "About the hotel": "Om hotellet", "Activities": "Aktiviteter", "Add code": "Lägg till kod", "Add new card": "Lägg till nytt kort", "Address": "Adress", "Airport": "Flygplats", + "Adults": "Vuxna", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", "Amusement park": "Nöjespark", + "An error occurred. Please try again.": "Ett fel uppstod. Försök igen.", "An error occurred trying to manage your preferences, please try again later.": "Ett fel uppstod när du försökte hantera dina inställningar, försök igen senare.", "An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.", "An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.", @@ -44,6 +47,7 @@ "Check in": "Checka in", "Check out": "Checka ut", "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.", + "Children": "Barn", "Choose room": "Välj rum", "Cities": "Städer", "City": "Ort", @@ -81,7 +85,9 @@ "Discard unsaved changes?": "Vill du ignorera ändringar som inte har sparats?", "Distance to city centre": "{number}km till centrum", "Do you want to start the day with Scandics famous breakfast buffé?": "Vill du starta dagen med Scandics berömda frukostbuffé?", + "Done": "Klar", "Download the Scandic app": "Ladda ner Scandic-appen", + "Driving directions": "Vägbeskrivningar", "Earn bonus nights & points": "Tjäna bonusnätter och poäng", "Edit": "Redigera", "Edit profile": "Redigera profil", @@ -99,8 +105,9 @@ "Fair": "Mässa", "Find booking": "Hitta bokning", "Find hotels": "Hitta hotell", - "Firstname": "Förnamn", + "First name": "Förnamn", "Flexibility": "Flexibilitet", + "Follow us": "Följ oss", "Former Scandic Hotel": "Tidigare Scandichotell", "Free cancellation": "Fri avbokning", "Free rebooking": "Fri ombokning", @@ -121,13 +128,16 @@ "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", "Image gallery": "Bildgalleri", + "In adults bed": "I vuxens säng", + "In crib": "I spjälsäng", + "In extra bed": "Egen sängplats", "Included": "Inkluderad", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", "Join Scandic Friends": "Gå med i Scandic Friends", "Join at no cost": "Gå med utan kostnad", "King bed": "King size-säng", "Language": "Språk", - "Lastname": "Efternamn", + "Last name": "Efternamn", "Latest searches": "Senaste sökningarna", "Left": "kvar", "Level": "Nivå", @@ -180,6 +190,7 @@ "No, keep card": "Nej, behåll kortet", "Non refundable": "Ej återbetalningsbar", "Non-refundable": "Ej återbetalningsbar", + "Nordic Swan Ecolabel": "Svanenmärkt", "Not found": "Hittades inte", "Nr night, nr adult": "{nights, number} natt, {adults, number} vuxen", "On your journey": "På din resa", @@ -222,6 +233,7 @@ "Retype new password": "Upprepa nytt lösenord", "Room & Terms": "Rum & Villkor", "Room facilities": "Rumfaciliteter", + "Room": "Rum", "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sauna and gym": "Sauna and gym", @@ -230,8 +242,10 @@ "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", "See hotel details": "Se hotellinformation", + "See less FAQ": "See färre FAQ", "See room details": "Se rumsdetaljer", "See rooms": "Se rum", "Select a country": "Välj ett land", @@ -246,8 +260,10 @@ "Shopping & Dining": "Shopping & Mat", "Show all amenities": "Visa alla bekvämligheter", "Show less": "Visa mindre", + "Show less rooms": "Visa färre rum", "Show map": "Visa karta", "Show more": "Visa mer", + "Show more rooms": "Visa fler rum", "Sign up bonus": "Välkomstbonus", "Sign up to Scandic Friends": "Bli medlem i Scandic Friends", "Skip to main content": "Fortsätt till huvudinnehåll", @@ -317,6 +333,7 @@ "Zoom out": "Zooma ut", "as of today": "från och med idag", "booking.adults": "{totalAdults, plural, one {# vuxen} other {# vuxna}}", + "booking.children": "{totalChildren, plural, one {# barn} other {# barn}}", "booking.guests": "Max {nrOfGuests, plural, one {# gäst} other {# gäster}}", "booking.nights": "{totalNights, plural, one {# natt} other {# nätter}}", "booking.rooms": "{totalRooms, plural, one {# rum} other {# rum}}", diff --git a/lib/graphql/Fragments/Blocks/Accordion.graphql b/lib/graphql/Fragments/Blocks/Accordion.graphql new file mode 100644 index 000000000..e77b26606 --- /dev/null +++ b/lib/graphql/Fragments/Blocks/Accordion.graphql @@ -0,0 +1,123 @@ +#import "../PageLink/AccountPageLink.graphql" +#import "../PageLink/ContentPageLink.graphql" +#import "../PageLink/HotelPageLink.graphql" +#import "../PageLink/LoyaltyPageLink.graphql" + +#import "../AccountPage/Ref.graphql" +#import "../ContentPage/Ref.graphql" +#import "../HotelPage/Ref.graphql" +#import "../LoyaltyPage/Ref.graphql" + +#import "./Refs/Accordion.graphql" + +fragment AccordionBlock on Accordion { + __typename + title + questions { + question + answer { + json + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + } +} + +fragment Accordion_ContentPage on ContentPageBlocksAccordion { + __typename + accordion { + title + accordions { + __typename + ...GlobalAccordion + ...SpecificAccordion + } + } +} + +fragment GlobalAccordion on ContentPageBlocksAccordionBlockAccordionsGlobalAccordion { + __typename + global_accordion { + global_accordionConnection { + edges { + node { + ...AccordionBlock + } + } + } + } +} + +fragment SpecificAccordion on ContentPageBlocksAccordionBlockAccordionsSpecificAccordion { + __typename + specific_accordion { + questions { + question + answer { + json + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + } + } +} + +fragment Accordion_ContentPageRefs on ContentPageBlocksAccordion { + accordion { + accordions { + __typename + ...GlobalAccordionRefs + ...SpecificAccordionRefs + } + } +} + +fragment GlobalAccordionRefs on ContentPageBlocksAccordionBlockAccordionsGlobalAccordion { + global_accordion { + global_accordionConnection { + edges { + node { + ...AccordionBlockRefs + } + } + } + } +} + +fragment SpecificAccordionRefs on ContentPageBlocksAccordionBlockAccordionsSpecificAccordion { + specific_accordion { + questions { + answer { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } + } +} diff --git a/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql b/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql new file mode 100644 index 000000000..453fe405e --- /dev/null +++ b/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql @@ -0,0 +1,22 @@ +#import "../../AccountPage/Ref.graphql" +#import "../../ContentPage/Ref.graphql" +#import "../../HotelPage/Ref.graphql" +#import "../../LoyaltyPage/Ref.graphql" + +fragment AccordionBlockRefs on Accordion { + questions { + answer { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } +} diff --git a/lib/graphql/Query/ContentPage/ContentPage.graphql b/lib/graphql/Query/ContentPage/ContentPage.graphql index 50df1c89e..5e91dc92f 100644 --- a/lib/graphql/Query/ContentPage/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage/ContentPage.graphql @@ -1,5 +1,6 @@ #import "../../Fragments/System.graphql" +#import "../../Fragments/Blocks/Accordion.graphql" #import "../../Fragments/Blocks/CardsGrid.graphql" #import "../../Fragments/Blocks/Content.graphql" #import "../../Fragments/Blocks/DynamicContent.graphql" @@ -7,7 +8,6 @@ #import "../../Fragments/Blocks/Table.graphql" #import "../../Fragments/Blocks/TextCols.graphql" #import "../../Fragments/Blocks/UspGrid.graphql" - #import "../../Fragments/ContentPage/NavigationLinks.graphql" #import "../../Fragments/Sidebar/Content.graphql" @@ -40,13 +40,22 @@ query GetContentPage($locale: String!, $uid: String!) { } } -query GetContentPageBlocks($locale: String!, $uid: String!) { +query GetContentPageBlocksBatch1($locale: String!, $uid: String!) { content_page(uid: $uid, locale: $locale) { blocks { __typename + ...Accordion_ContentPage ...CardsGrid_ContentPage ...Content_ContentPage ...DynamicContent_ContentPage + } + } +} + +query GetContentPageBlocksBatch2($locale: String!, $uid: String!) { + content_page(uid: $uid, locale: $locale) { + blocks { + __typename ...Shortcuts_ContentPage ...Table_ContentPage ...TextCols_ContentPage @@ -73,6 +82,7 @@ query GetContentPageRefs($locale: String!, $uid: String!) { } blocks { __typename + ...Accordion_ContentPageRefs ...CardsGrid_ContentPageRefs ...Content_ContentPageRefs ...DynamicContent_ContentPageRefs diff --git a/lib/graphql/Query/HotelPage/HotelPage.graphql b/lib/graphql/Query/HotelPage/HotelPage.graphql index 6bd6131c1..1157f4fef 100644 --- a/lib/graphql/Query/HotelPage/HotelPage.graphql +++ b/lib/graphql/Query/HotelPage/HotelPage.graphql @@ -1,10 +1,47 @@ +#import "../../Fragments/PageLink/AccountPageLink.graphql" #import "../../Fragments/PageLink/ContentPageLink.graphql" +#import "../../Fragments/PageLink/HotelPageLink.graphql" +#import "../../Fragments/PageLink/LoyaltyPageLink.graphql" + +#import "../../Fragments/Blocks/Accordion.graphql" query GetHotelPage($locale: String!, $uid: String!) { hotel_page(locale: $locale, uid: $uid) { hotel_page_id title url + faq { + __typename + title + global_faqConnection { + __typename + edges { + node { + ...AccordionBlock + } + } + } + specific_faq { + __typename + questions { + question + answer { + json + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageLink + ...ContentPageLink + ...HotelPageLink + ...LoyaltyPageLink + } + } + } + } + } + } + } content { __typename ... on HotelPageContentUpcomingActivitiesCard { @@ -26,6 +63,60 @@ query GetHotelPage($locale: String!, $uid: String!) { } } } + system { + ...System + created_at + updated_at + } + } +} + +query GetHotelPageRefs($locale: String!, $uid: String!) { + hotel_page(locale: $locale, uid: $uid) { + faq { + global_faqConnection { + edges { + node { + ...AccordionBlockRefs + } + } + } + specific_faq { + questions { + answer { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } + } + } + content { + __typename + ... on HotelPageContentUpcomingActivitiesCard { + upcoming_activities_card { + hotel_page_activities_content_pageConnection { + edges { + node { + __typename + ...ContentPageRef + } + } + } + } + } + } + system { + ...System + } } trackingProps: hotel_page(locale: "en", uid: $uid) { url diff --git a/lib/graphql/_request.ts b/lib/graphql/_request.ts index 96246e453..4040dfd9e 100644 --- a/lib/graphql/_request.ts +++ b/lib/graphql/_request.ts @@ -130,6 +130,7 @@ export async function request( * version for one language, it throws an error which we have to recover * from here since it isn't an error. */ + return { data: error.response.data as T } } } diff --git a/lint-staged.config.js b/lint-staged.config.js index 3fe2bfffa..1959b0716 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -10,6 +10,7 @@ const config = { "*.{js,jsx,ts,tsx}": [buildEslintCommand], "*.{ts,tsx}": () => "tsc -p tsconfig.json --noEmit", "*": "prettier --write", + "i18n/dictionaries/*.json": "jsonsort", } export default config diff --git a/middleware.ts b/middleware.ts index 8aaff393e..489d30503 100644 --- a/middleware.ts +++ b/middleware.ts @@ -15,6 +15,10 @@ import * as webView from "@/middlewares/webView" import { findLang } from "@/utils/languages" export const middleware: NextMiddleware = async (request, event) => { + // auth() overrides the request origin, we need the original for internal rewrites + // @see getInternalNextURL() + request.headers.set("x-sh-origin", request.nextUrl.origin) + const headers = getDefaultRequestHeaders(request) const lang = findLang(request.nextUrl.pathname) @@ -60,6 +64,8 @@ export const middleware: NextMiddleware = async (request, event) => { if (_continue) { continue } + // Clean up internal headers + result?.headers.delete("x-sh-origin") return result } } diff --git a/middlewares/authRequired.ts b/middlewares/authRequired.ts index 0e50330ee..c0948cd6c 100644 --- a/middlewares/authRequired.ts +++ b/middlewares/authRequired.ts @@ -2,8 +2,7 @@ import { NextResponse } from "next/server" import { authRequired, mfaRequired } from "@/constants/routes/authRequired" import { login } from "@/constants/routes/handleAuth" -import { env } from "@/env/server" -import { internalServerError } from "@/server/errors/next" +import { getInternalNextURL, getPublicNextURL } from "@/server/utils" import { auth } from "@/auth" import { findLang } from "@/utils/languages" @@ -38,20 +37,16 @@ import type { MiddlewareMatcher } from "@/types/middleware" * https://authjs.dev/reference/nextjs */ export const middleware = auth(async (request) => { - const { nextUrl } = request - const lang = findLang(nextUrl.pathname)! + const lang = findLang(request.nextUrl.pathname)! const isLoggedIn = !!request.auth const hasError = request.auth?.error - if (!env.PUBLIC_URL) { - throw internalServerError("Missing value for env.PUBLIC_URL") - } + // Inside auth() we need an internal request for rewrites. + // @see getInternalNextURL() + const nextUrlInternal = getInternalNextURL(request) - const publicUrl = new URL(env.PUBLIC_URL) - const nextUrlPublic = nextUrl.clone() - nextUrlPublic.host = publicUrl.host - nextUrlPublic.hostname = publicUrl.hostname + const nextUrlPublic = getPublicNextURL(request) /** * Function to validate MFA from token data @@ -63,13 +58,13 @@ export const middleware = auth(async (request) => { : false return !(request.auth?.token.mfa_scope && isMFATokenValid) } - const isMFAPath = mfaRequired.includes(nextUrl.pathname) + const isMFAPath = mfaRequired.includes(request.nextUrl.pathname) if (isLoggedIn && isMFAPath && isMFAInvalid()) { const headers = new Headers(request.headers) headers.set("x-returnurl", nextUrlPublic.href) headers.set("x-login-source", "mfa") - return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { + return NextResponse.rewrite(new URL(`/${lang}/login`, nextUrlInternal), { request: { headers, }, diff --git a/middlewares/currentWebLogout.ts b/middlewares/currentWebLogout.ts index c358012f2..2e52346a2 100644 --- a/middlewares/currentWebLogout.ts +++ b/middlewares/currentWebLogout.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server" import { env } from "@/env/server" import { badRequest, internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { findLang } from "@/utils/languages" @@ -16,10 +17,7 @@ export const middleware: NextMiddleware = (request) => { } const lang = findLang(request.nextUrl.pathname)! - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } - const redirectTo = env.PUBLIC_URL + const redirectTo = getPublicURL(request) const headers = new Headers(request.headers) headers.set("x-returnurl", redirectTo) diff --git a/middlewares/myPages.ts b/middlewares/myPages.ts index 28c375f13..6e27f10a9 100644 --- a/middlewares/myPages.ts +++ b/middlewares/myPages.ts @@ -8,6 +8,7 @@ import { } from "@/constants/routes/myPages" import { env } from "@/env/server" import { internalServerError, notFound } from "@/server/errors/next" +import { getPublicNextURL } from "@/server/utils" import { findLang } from "@/utils/languages" @@ -23,17 +24,9 @@ export const middleware: NextMiddleware = async (request) => { const myPagesRoot = myPages[lang] if (nextUrl.pathname === myPagesRoot) { - if (!env.PUBLIC_URL) { - throw internalServerError("Missing value for env.PUBLIC_URL") - } - - const publicUrl = new URL(env.PUBLIC_URL) - const nextUrlClone = nextUrl.clone() - nextUrlClone.host = publicUrl.host - nextUrlClone.hostname = publicUrl.hostname - + const nextUrlPublic = getPublicNextURL(request) const overviewUrl = overview[lang] - const redirectUrl = new URL(overviewUrl, nextUrlClone) + const redirectUrl = new URL(overviewUrl, nextUrlPublic) console.log(`[myPages] redirecting to: ${redirectUrl}`) return NextResponse.redirect(redirectUrl) } diff --git a/middlewares/utils.ts b/middlewares/utils.ts index 91e06a68d..18714d28f 100644 --- a/middlewares/utils.ts +++ b/middlewares/utils.ts @@ -1,7 +1,5 @@ -import { stringify } from "querystring" - import { Lang } from "@/constants/languages" -import { env } from "@/env/server" +import { getPublicNextURL } from "@/server/utils" import { resolve as resolveEntry } from "@/utils/entry" import { findLang } from "@/utils/languages" @@ -11,18 +9,7 @@ import type { NextRequest } from "next/server" export function getDefaultRequestHeaders(request: NextRequest) { const lang = findLang(request.nextUrl.pathname)! - - let nextUrl - if (env.PUBLIC_URL) { - const publicUrl = new URL(env.PUBLIC_URL) - const nextUrlPublic = request.nextUrl.clone() - nextUrlPublic.host = publicUrl.host - nextUrlPublic.hostname = publicUrl.hostname - nextUrl = nextUrlPublic - } else { - nextUrl = request.nextUrl - } - + const nextUrlPublic = getPublicNextURL(request) const headers = new Headers(request.headers) headers.set("x-lang", lang) headers.set( @@ -31,7 +18,7 @@ export function getDefaultRequestHeaders(request: NextRequest) { request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "") ) ) - headers.set("x-url", removeTrailingSlash(nextUrl.href)) + headers.set("x-url", removeTrailingSlash(nextUrlPublic.href)) return headers } diff --git a/netlify.toml b/netlify.toml index 022eee2ee..850405109 100644 --- a/netlify.toml +++ b/netlify.toml @@ -30,6 +30,3 @@ package = "@netlify/plugin-nextjs" [images] remote_images = ["https://imagevault-stage.scandichotels.com.*", "https://imagevault.scandichotels.com.*"] - -[functions] -included_files = ["./.env"] diff --git a/next.config.js b/next.config.js index b11c9c466..46f898e2f 100644 --- a/next.config.js +++ b/next.config.js @@ -72,66 +72,6 @@ const nextConfig = { // https://nextjs.org/docs/app/api-reference/next-config-js/redirects#header-cookie-and-query-matching redirects() { - // const rateQueries = [ - // { - // key: "hotel", - // type: "query", - // value: undefined, - // }, - // { - // key: "fromdate", - // type: "query", - // value: undefined, - // }, - // { - // key: "todate", - // type: "query", - // value: undefined, - // }, - // { - // key: "room", - // type: "query", - // value: undefined, - // }, - // ] - const bedTypeQuery = { - key: "bedType", - type: "query", - value: undefined, - } - const breakfastQuery = { - key: "breakfast", - type: "query", - value: undefined, - } - const detailQueries = [ - { - key: "countryCode", - type: "query", - value: undefined, - }, - { - key: "email", - type: "query", - value: undefined, - }, - { - key: "firstname", - type: "query", - value: undefined, - }, - { - key: "lastname", - type: "query", - value: undefined, - }, - { - key: "phoneNumber", - type: "query", - value: undefined, - }, - ] - return [ // { // ---------------------------------------- @@ -140,43 +80,84 @@ const nextConfig = { // ---------------------------------------- // source: "/:lang/hotelreservation/(select-bed|breakfast|details|payment)", // destination: "/:lang/hotelreservation/select-rate", - // missing: rateQueries, + // missing: [ + // { + // key: "hotel", + // type: "query", + // value: undefined, + // }, + // { + // key: "fromdate", + // type: "query", + // value: undefined, + // }, + // { + // key: "todate", + // type: "query", + // value: undefined, + // }, + // { + // key: "room", + // type: "query", + // value: undefined, + // }, + // ], // permanent: false, // }, { - source: "/:lang/hotelreservation/breakfast", + source: "/:lang/hotelreservation/(breakfast|details|payment)", destination: "/:lang/hotelreservation/select-bed", - missing: [bedTypeQuery], + missing: [ + { + key: "bedType", + type: "query", + value: undefined, + }, + ], permanent: false, }, { - source: "/:lang/hotelreservation/details", - destination: "/:lang/hotelreservation/select-bed", - missing: [bedTypeQuery], - permanent: false, - }, - { - source: "/:lang/hotelreservation/details", + source: "/:lang/hotelreservation/(details|payment)", destination: "/:lang/hotelreservation/breakfast", - missing: [breakfastQuery], - permanent: false, - }, - { - source: "/:lang/hotelreservation/payment", - destination: "/:lang/hotelreservation/select-bed", - missing: [bedTypeQuery], - permanent: false, - }, - { - source: "/:lang/hotelreservation/payment", - destination: "/:lang/hotelreservation/breakfast", - missing: [breakfastQuery], + missing: [ + { + key: "breakfast", + type: "query", + value: undefined, + }, + ], permanent: false, }, { source: "/:lang/hotelreservation/payment", destination: "/:lang/hotelreservation/details", - missing: detailQueries, + missing: [ + { + key: "countryCode", + type: "query", + value: undefined, + }, + { + key: "email", + type: "query", + value: undefined, + }, + { + key: "firstname", + type: "query", + value: undefined, + }, + { + key: "lastname", + type: "query", + value: undefined, + }, + { + key: "phoneNumber", + type: "query", + value: undefined, + }, + ], permanent: false, }, ] diff --git a/package-lock.json b/package-lock.json index db6a1ca42..9cd999fd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jiti": "^1.21.0", + "json-sort-cli": "^4.0.4", "lint-staged": "^15.2.2", "netlify-plugin-cypress": "^2.2.1", "prettier": "^3.2.5", @@ -3373,9 +3374,9 @@ } }, "node_modules/@netlify/plugin-nextjs": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@netlify/plugin-nextjs/-/plugin-nextjs-5.1.1.tgz", - "integrity": "sha512-r9Z2of/hXZAgnk8br3V9+JJlXrolm+cbAMXcVRaA23+F6k19ZLgknqa2ytUMlAX+NnKk38si4MGXAutLe8ju/A==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@netlify/plugin-nextjs/-/plugin-nextjs-5.8.0.tgz", + "integrity": "sha512-1hduTMFYkZRSqRAPK1+5A6T/uc543PfxX0erzJEVWb9fKgPlY24ZQMi2nXBwlQ6JlBYjZJcMEtN2vhhNqyjgXA==", "engines": { "node": ">=18.0.0" } @@ -3717,6 +3718,47 @@ "node": ">=14" } }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", @@ -5601,6 +5643,18 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -6838,6 +6892,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -7176,6 +7239,29 @@ "node": ">=0.8" } }, + "node_modules/ast-monkey-traverse": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/ast-monkey-traverse/-/ast-monkey-traverse-4.0.16.tgz", + "integrity": "sha512-ky+cFh4o+TTzQJTrT0pP3MG6E9RP3qYHtF7NGAXgOzP0MXIATw+8sOdMnVd8f138PXgdRBX642RjN3Eb6xnkTw==", + "dev": true, + "dependencies": { + "ast-monkey-util": "^3.0.8", + "codsen-utils": "^1.6.4", + "rfdc": "^1.3.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ast-monkey-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/ast-monkey-util/-/ast-monkey-util-3.0.8.tgz", + "integrity": "sha512-puSRInugkQtIX8hsbXp4jJzok7yMuLqg1kpmTCsqxpbkSiLKI88p+Mx5u9+7CanydNRhmsH/1cGTJvvGRuRfpQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -7212,6 +7298,16 @@ "node": ">= 4.0.0" } }, + "node_modules/atomically": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", + "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", + "dev": true, + "dependencies": { + "stubborn-fs": "^1.2.5", + "when-exit": "^2.1.1" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -7574,6 +7670,143 @@ "dev": true, "license": "ISC" }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7966,6 +8199,18 @@ "node": ">=6" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -8096,6 +8341,18 @@ "type-is": "^1.6.16" } }, + "node_modules/codsen-utils": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/codsen-utils/-/codsen-utils-1.6.4.tgz", + "integrity": "sha512-PDyvQ5f2PValmqZZIJATimcokDt4JjIev8cKbZgEOoZm+U1IJDYuLeTcxZPQdep99R/X0RIlQ6ReQgPOVnPbNw==", + "dev": true, + "dependencies": { + "rfdc": "^1.3.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -8320,6 +8577,40 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/configstore": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.0.0.tgz", + "integrity": "sha512-yk7/5PN5im4qwz0WFZW3PXnzHgPu9mX29Y8uZ3aefe2lBPC1FYttWZRcaW9fKkT0pBCJyuQ2HfbmPVaODi9jcQ==", + "dev": true, + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -8974,6 +9265,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-indent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.1.tgz", + "integrity": "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -9135,6 +9435,33 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "dev": true, + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -9476,6 +9803,18 @@ "node": ">=6" } }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -10524,6 +10863,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -10586,6 +10937,15 @@ "assert-plus": "^1.0.0" } }, + "node_modules/git-hooks-list": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-3.1.0.tgz", + "integrity": "sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==", + "dev": true, + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -10644,6 +11004,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -11350,6 +11734,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-d/-/is-d-1.0.0.tgz", + "integrity": "sha512-IK3iBV/PkPB/CHpzt/37n+Wr1jpg7H/oSw2TKaNDS2dbLRwP7EiH7AToDWdySa3TNXgCO8Wdl5llPJYsE7Mb+Q==", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-data-view": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", @@ -11461,6 +11857,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz", + "integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==", + "dev": true, + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -11501,6 +11912,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-npm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", + "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -11534,6 +11957,18 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -13030,6 +13465,102 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-sort-cli": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/json-sort-cli/-/json-sort-cli-4.0.4.tgz", + "integrity": "sha512-DdlQQuQXu8Deujb+A/ECcUVmC/uAWciDKSLB3lES4Q60gOYCf9D1gesMUMPZczUX7YUPupjIG4J4e5hX6VGZWA==", + "dev": true, + "dependencies": { + "ast-monkey-traverse": "^4.0.16", + "chalk": "^5.3.0", + "codsen-utils": "^1.6.4", + "fs-extra": "^11.2.0", + "globby": "^14.0.1", + "is-d": "^1.0.0", + "meow": "^13.2.0", + "p-filter": "^4.1.0", + "p-reduce": "^3.0.0", + "sort-package-json": "^2.10.0", + "update-notifier": "^7.0.0" + }, + "bin": { + "jsonsort": "cli.js", + "sortjson": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/json-sort-cli/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/json-sort-cli/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/json-sort-cli/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-sort-cli/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-sort-cli/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -13353,6 +13884,18 @@ "ms": "^2.1.1" } }, + "node_modules/ky": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/ky/-/ky-1.7.2.tgz", + "integrity": "sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -13371,6 +13914,21 @@ "node": ">=0.10" } }, + "node_modules/latest-version": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz", + "integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==", + "dev": true, + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -14482,6 +15040,18 @@ "node": ">= 0.6" } }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -15235,6 +15805,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-filter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", + "dev": true, + "dependencies": { + "p-map": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-filter/node_modules/p-map": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.2.tgz", + "integrity": "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -15289,6 +15886,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-timeout": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", @@ -15310,6 +15919,24 @@ "node": ">=6" } }, + "node_modules/package-json": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", + "integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==", + "dev": true, + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -15494,6 +16121,27 @@ "node": ">=0.10.0" } }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -15824,6 +16472,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "node_modules/proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -15877,6 +16531,21 @@ "node": ">=6" } }, + "node_modules/pupa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", + "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", + "dev": true, + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/puppeteer": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-18.1.0.tgz", @@ -16048,6 +16717,36 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -16487,6 +17186,33 @@ "node": ">=4" } }, + "node_modules/registry-auth-token": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", + "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "dev": true, + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", @@ -16880,12 +17606,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -16893,17 +17616,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/serve-index-75lb": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/serve-index-75lb/-/serve-index-75lb-2.0.1.tgz", @@ -17117,6 +17829,74 @@ "react-dom": "^18.0.0" } }, + "node_modules/sort-object-keys": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", + "integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==", + "dev": true + }, + "node_modules/sort-package-json": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-2.10.1.tgz", + "integrity": "sha512-d76wfhgUuGypKqY72Unm5LFnMpACbdxXsLPcL27pOsSrmVqH3PztFp1uq+Z22suk15h7vXmTesuh2aEjdCqb5w==", + "dev": true, + "dependencies": { + "detect-indent": "^7.0.1", + "detect-newline": "^4.0.0", + "get-stdin": "^9.0.0", + "git-hooks-list": "^3.0.0", + "globby": "^13.1.2", + "is-plain-obj": "^4.1.0", + "semver": "^7.6.0", + "sort-object-keys": "^1.1.3" + }, + "bin": { + "sort-package-json": "cli.js" + } + }, + "node_modules/sort-package-json/node_modules/detect-newline": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-4.0.1.tgz", + "integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-package-json/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-package-json/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17568,6 +18348,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubborn-fs": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", + "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==", + "dev": true + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -18372,6 +19158,18 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -18430,6 +19228,70 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-notifier": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", + "dev": true, + "dependencies": { + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -18657,6 +19519,12 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/when-exit": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.3.tgz", + "integrity": "sha512-uVieSTccFIr/SFQdFWN/fFaQYmV37OKtuaGphMAzi4DmmUlrvRBJW5WSLkHyjNQY/ePJMz3LoiX9R3yy1Su6Hw==", + "dev": true + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -18751,6 +19619,71 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "dev": true, + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wordwrapjs": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", @@ -18913,6 +19846,18 @@ } } }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -18937,11 +19882,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yaml": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", diff --git a/package.json b/package.json index 669ab58da..0f8bd14a2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "prebuild": "npm run update-dotenv && npm run lint && npm run test:unit", + "prebuild": "npm run lint && npm run test:unit", "build": "next build", "predev": "rm -rf .next", "dev": "PORT=3000 NEXT_PUBLIC_PORT=3000 next dev", @@ -20,7 +20,6 @@ "test:e2e:headless": "start-server-and-test test:setup http://127.0.0.1:3000/en/sponsoring \"cypress run --e2e\"", "test:setup": "npm run build && npm run start", "preinstall": "export $(cat .env.local | grep -v '^#' | xargs)", - "update-dotenv": "node update-dotenv.mjs", "test:unit": "jest", "test:unit:watch": "jest --watch" }, @@ -91,6 +90,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jiti": "^1.21.0", + "json-sort-cli": "^4.0.4", "lint-staged": "^15.2.2", "netlify-plugin-cypress": "^2.2.1", "prettier": "^3.2.5", diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 3901a6992..da500601d 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -2,6 +2,10 @@ import { z } from "zod" import { discriminatedUnionArray } from "@/lib/discriminatedUnion" +import { + accordionRefsSchema, + accordionSchema, +} from "../schemas/blocks/accordion" import { cardGridRefsSchema, cardsGridSchema, @@ -81,8 +85,14 @@ export const contentPageTable = z __typename: z.literal(ContentPageEnum.ContentStack.blocks.Table), }) .merge(tableSchema) +export const contentPageAccordion = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.Accordion), + }) + .merge(accordionSchema) export const blocksSchema = z.discriminatedUnion("__typename", [ + contentPageAccordion, contentPageCards, contentPageContent, contentPageDynamicContent, @@ -158,6 +168,12 @@ export const contentPageSchema = z.object({ }), }) +export const contentPageSchemaBlocks = z.object({ + content_page: z.object({ + blocks: discriminatedUnionArray(blocksSchema.options).nullable(), + }), +}) + /** REFS */ const contentPageCardsRefs = z .object({ @@ -195,7 +211,14 @@ const contentPageUspGridRefs = z }) .merge(uspGridRefsSchema) +const contentPageAccordionRefs = z + .object({ + __typename: z.literal(ContentPageEnum.ContentStack.blocks.Accordion), + }) + .merge(accordionRefsSchema) + const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ + contentPageAccordionRefs, contentPageBlockContentRefs, contentPageShortcutsRefs, contentPageCardsRefs, diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index abce4d91a..a7e5d53f0 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -1,7 +1,8 @@ import { Lang } from "@/constants/languages" import { GetContentPage, - GetContentPageBlocks, + GetContentPageBlocksBatch1, + GetContentPageBlocksBatch2, } from "@/lib/graphql/Query/ContentPage/ContentPage.graphql" import { request } from "@/lib/graphql/request" import { contentstackExtendedProcedureUID, router } from "@/server/trpc" @@ -25,6 +26,7 @@ export const contentPageQueryRouter = router({ const { lang, uid } = ctx const contentPageRefsData = await fetchContentPageRefs(lang, uid) + const contentPageRefs = validateContentPageRefs( contentPageRefsData, lang, @@ -33,6 +35,7 @@ export const contentPageQueryRouter = router({ if (!contentPageRefs) { return null } + const tags = generatePageTags(contentPageRefs, lang) getContentPageCounter.add(1, { lang, uid }) @@ -43,7 +46,7 @@ export const contentPageQueryRouter = router({ }) ) - const [mainResponse, blocksResponse] = await Promise.all([ + const [mainResponse, blocksResponse1, blocksResponse2] = await Promise.all([ request( GetContentPage, { locale: lang, uid }, @@ -55,7 +58,17 @@ export const contentPageQueryRouter = router({ } ), request( - GetContentPageBlocks, + GetContentPageBlocksBatch1, + { locale: lang, uid }, + { + cache: "force-cache", + next: { + tags, + }, + } + ), + request( + GetContentPageBlocksBatch2, { locale: lang, uid }, { cache: "force-cache", @@ -70,7 +83,12 @@ export const contentPageQueryRouter = router({ ...mainResponse.data, content_page: { ...mainResponse.data.content_page, - blocks: blocksResponse.data.content_page.blocks, + blocks: [ + blocksResponse1.data.content_page.blocks, + blocksResponse2.data.content_page.blocks, + ] + .flat(2) + .filter((obj) => !(obj && Object.keys(obj).length < 2)), // Remove empty objects and objects with only typename }, } diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts index 78f5a9d7f..30a947717 100644 --- a/server/routers/contentstack/contentPage/utils.ts +++ b/server/routers/contentstack/contentPage/utils.ts @@ -126,6 +126,12 @@ export function getConnections({ content_page }: ContentPageRefs) { if (content_page.blocks) { content_page.blocks.forEach((block) => { switch (block.__typename) { + case ContentPageEnum.ContentStack.blocks.Accordion: { + if (block.accordion.length) { + connections.push(...block.accordion) + } + break + } case ContentPageEnum.ContentStack.blocks.Content: { if (block.content.length) { diff --git a/server/routers/contentstack/hotelPage/output.ts b/server/routers/contentstack/hotelPage/output.ts index f57bb479d..3ed665165 100644 --- a/server/routers/contentstack/hotelPage/output.ts +++ b/server/routers/contentstack/hotelPage/output.ts @@ -2,7 +2,12 @@ import { z } from "zod" import { discriminatedUnionArray } from "@/lib/discriminatedUnion" -import { activitiesCard } from "../schemas/blocks/activitiesCard" +import { + activitiesCardRefSchema, + activitiesCardSchema, +} from "../schemas/blocks/activitiesCard" +import { hotelFaqRefsSchema, hotelFaqSchema } from "../schemas/blocks/hotelFaq" +import { systemSchema } from "../schemas/system" import { HotelPageEnum } from "@/types/enums/hotelPage" @@ -10,7 +15,7 @@ const contentBlockActivities = z .object({ __typename: z.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard), }) - .merge(activitiesCard) + .merge(activitiesCardSchema) export const contentBlock = z.discriminatedUnion("__typename", [ contentBlockActivities, @@ -19,9 +24,35 @@ export const contentBlock = z.discriminatedUnion("__typename", [ export const hotelPageSchema = z.object({ hotel_page: z.object({ content: discriminatedUnionArray(contentBlock.options).nullable(), + faq: hotelFaqSchema, hotel_page_id: z.string(), title: z.string(), url: z.string(), + system: systemSchema.merge( + z.object({ + created_at: z.string(), + updated_at: z.string(), + }) + ), + }), +}) + +/** REFS */ +const hotelPageActiviesCardRefs = z + .object({ + __typename: z.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard), + }) + .merge(activitiesCardRefSchema) + +const hotelPageBlockRefsItem = z.discriminatedUnion("__typename", [ + hotelPageActiviesCardRefs, +]) + +export const hotelPageRefsSchema = z.object({ + hotel_page: z.object({ + content: discriminatedUnionArray(hotelPageBlockRefsItem.options).nullable(), + faq: hotelFaqRefsSchema.nullable(), + system: systemSchema, }), trackingProps: z.object({ url: z.string(), diff --git a/server/routers/contentstack/hotelPage/utils.ts b/server/routers/contentstack/hotelPage/utils.ts new file mode 100644 index 000000000..1b3a0adfe --- /dev/null +++ b/server/routers/contentstack/hotelPage/utils.ts @@ -0,0 +1,142 @@ +import { metrics } from "@opentelemetry/api" + +import { Lang } from "@/constants/languages" +import { GetHotelPageRefs } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql" +import { request } from "@/lib/graphql/request" +import { notFound } from "@/server/errors/trpc" + +import { generateTag, generateTagsFromSystem } from "@/utils/generateTag" + +import { hotelPageRefsSchema } from "./output" + +import { HotelPageEnum } from "@/types/enums/hotelPage" +import { System } from "@/types/requests/system" +import { + GetHotelPageRefsSchema, + HotelPageRefs, +} from "@/types/trpc/routers/contentstack/hotelPage" + +const meter = metrics.getMeter("trpc.hotelPage") +// OpenTelemetry metrics: HotelPage + +export const getHotelPageCounter = meter.createCounter( + "trpc.contentstack.hotelPage.get" +) + +const getHotelPageRefsCounter = meter.createCounter( + "trpc.contentstack.hotelPage.get" +) +const getHotelPageRefsFailCounter = meter.createCounter( + "trpc.contentstack.hotelPage.get-fail" +) +const getHotelPageRefsSuccessCounter = meter.createCounter( + "trpc.contentstack.hotelPage.get-success" +) + +export async function fetchHotelPageRefs(lang: Lang, uid: string) { + getHotelPageRefsCounter.add(1, { lang, uid }) + console.info( + "contentstack.hotelPage.refs start", + JSON.stringify({ + query: { lang, uid }, + }) + ) + + const refsResponse = await request( + GetHotelPageRefs, + { locale: lang, uid }, + { + cache: "force-cache", + next: { + tags: [generateTag(lang, uid)], + }, + } + ) + if (!refsResponse.data) { + const notFoundError = notFound(refsResponse) + getHotelPageRefsFailCounter.add(1, { + lang, + uid, + error_type: "http_error", + error: JSON.stringify({ + code: notFoundError.code, + }), + }) + console.error( + "contentstack.hotelPage.refs not found error", + JSON.stringify({ + query: { + lang, + uid, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + return refsResponse.data +} + +export function validateHotelPageRefs( + data: GetHotelPageRefsSchema, + lang: Lang, + uid: string +) { + const validatedData = hotelPageRefsSchema.safeParse(data) + if (!validatedData.success) { + getHotelPageRefsFailCounter.add(1, { + lang, + uid, + error_type: "validation_error", + error: JSON.stringify(validatedData.error), + }) + console.error( + "contentstack.hotelPage.refs validation error", + JSON.stringify({ + query: { lang, uid }, + error: validatedData.error, + }) + ) + return null + } + getHotelPageRefsSuccessCounter.add(1, { lang, uid }) + console.info( + "contentstack.hotelPage.refs success", + JSON.stringify({ + query: { lang, uid }, + }) + ) + + return validatedData.data +} + +export function generatePageTags( + validatedData: HotelPageRefs, + lang: Lang +): string[] { + const connections = getConnections(validatedData) + return [ + generateTagsFromSystem(lang, connections), + generateTag(lang, validatedData.hotel_page.system.uid), + ].flat() +} + +export function getConnections({ hotel_page }: HotelPageRefs) { + const connections: System["system"][] = [hotel_page.system] + if (hotel_page.content) { + hotel_page.content.forEach((block) => { + switch (block.__typename) { + case HotelPageEnum.ContentStack.blocks.ActivitiesCard: { + if (block.upcoming_activities_card.length) { + connections.push(...block.upcoming_activities_card) + } + break + } + } + if (hotel_page.faq) { + connections.push(...hotel_page.faq) + } + }) + } + return connections +} diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index f6af86315..85470553a 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -97,6 +97,8 @@ const getAllCachedApiRewards = unstable_cache( }, }) ) + + throw apiResponse } const data = await apiResponse.json() @@ -114,7 +116,7 @@ const getAllCachedApiRewards = unstable_cache( error: validatedApiTierRewards.error, }) ) - return null + throw validatedApiTierRewards.error } return validatedApiTierRewards.data diff --git a/server/routers/contentstack/schemas/blocks/accordion.ts b/server/routers/contentstack/schemas/blocks/accordion.ts new file mode 100644 index 000000000..c593c5c22 --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/accordion.ts @@ -0,0 +1,188 @@ +import { z } from "zod" + +import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks" + +import { BlocksEnums } from "@/types/enums/blocks" + +export const accordionItemsSchema = z.array( + z.object({ + question: z.string(), + answer: z.object({ + json: z.any(), // JSON + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: z + .discriminatedUnion("__typename", [ + pageLinks.accountPageSchema, + pageLinks.contentPageSchema, + pageLinks.hotelPageSchema, + pageLinks.loyaltyPageSchema, + ]) + .transform((data) => { + const link = pageLinks.transform(data) + if (link) { + return link + } + return data + }), + }) + ), + }), + }), + }) +) + +export type Accordion = z.infer + +enum AccordionEnum { + ContentPageBlocksAccordionBlockAccordionsGlobalAccordion = "ContentPageBlocksAccordionBlockAccordionsGlobalAccordion", + ContentPageBlocksAccordionBlockAccordionsSpecificAccordion = "ContentPageBlocksAccordionBlockAccordionsSpecificAccordion", +} + +export const accordionSchema = z.object({ + typename: z + .literal(BlocksEnums.block.Accordion) + .optional() + .default(BlocksEnums.block.Accordion), + accordion: z + .object({ + title: z.string().optional().default(""), + accordions: z.array( + z.object({ + __typename: z.enum([ + AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion, + AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion, + ]), + global_accordion: z + .object({ + global_accordionConnection: z.object({ + edges: z.array( + z.object({ + node: z.object({ + questions: accordionItemsSchema, + }), + }) + ), + }), + }) + .optional(), + specific_accordion: z + .object({ + questions: accordionItemsSchema, + }) + .optional(), + }) + ), + }) + .transform((data) => { + return { + ...data, + accordions: data.accordions.flatMap((acc) => { + switch (acc.__typename) { + case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion: + return ( + acc.global_accordion?.global_accordionConnection.edges.flatMap( + ({ node: accordionConnection }) => { + return accordionConnection.questions + } + ) || [] + ) + case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion: + return acc.specific_accordion?.questions || [] + } + }), + } + }), +}) + +const actualRefs = z.discriminatedUnion("__typename", [ + pageLinks.accountPageRefSchema, + pageLinks.contentPageRefSchema, + pageLinks.hotelPageRefSchema, + pageLinks.loyaltyPageRefSchema, +]) + +export const globalAccordionConnectionRefs = z.object({ + edges: z.array( + z.object({ + node: z.object({ + questions: z.array( + z.object({ + answer: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: actualRefs, + }) + ), + }), + }), + }) + ), + }), + }) + ), +}) + +export const specificAccordionConnectionRefs = z.object({ + questions: z.array( + z.object({ + answer: z.object({ + embedded_itemsConnection: z.object({ + edges: z.array( + z.object({ + node: actualRefs, + }) + ), + }), + }), + }) + ), +}) + +export const accordionRefsSchema = z.object({ + accordion: z + .object({ + accordions: z.array( + z.object({ + __typename: z.enum([ + AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion, + AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion, + ]), + global_accordion: z + .object({ + global_accordionConnection: globalAccordionConnectionRefs, + }) + .optional(), + specific_accordion: specificAccordionConnectionRefs.optional(), + }) + ), + }) + .transform((data) => { + return data.accordions.flatMap((accordion) => { + switch (accordion.__typename) { + case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion: + return ( + accordion.global_accordion?.global_accordionConnection.edges.flatMap( + ({ node: accordionConnection }) => { + return accordionConnection.questions.flatMap((question) => + question.answer.embedded_itemsConnection.edges.flatMap( + ({ node }) => node.system + ) + ) + } + ) || [] + ) + case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion: + return ( + accordion.specific_accordion?.questions.flatMap((question) => + question.answer.embedded_itemsConnection.edges.flatMap( + ({ node }) => node.system + ) + ) || [] + ) + } + }) + }), +}) diff --git a/server/routers/contentstack/schemas/blocks/activitiesCard.ts b/server/routers/contentstack/schemas/blocks/activitiesCard.ts index 0d4fba2b1..079414948 100644 --- a/server/routers/contentstack/schemas/blocks/activitiesCard.ts +++ b/server/routers/contentstack/schemas/blocks/activitiesCard.ts @@ -8,7 +8,7 @@ import { tempImageVaultAssetSchema } from "../imageVault" import { HotelPageEnum } from "@/types/enums/hotelPage" -export const activitiesCard = z.object({ +export const activitiesCardSchema = z.object({ typename: z .literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard) .optional() @@ -57,3 +57,25 @@ export const activitiesCard = z.object({ } }), }) + +export const activitiesCardRefSchema = z.object({ + upcoming_activities_card: z + .object({ + hotel_page_activities_content_pageConnection: z.object({ + edges: z.array( + z.object({ + node: z.discriminatedUnion("__typename", [ + pageLinks.contentPageRefSchema, + ]), + }) + ), + }), + }) + .transform((data) => { + return ( + data.hotel_page_activities_content_pageConnection.edges.flatMap( + ({ node }) => node.system + ) || [] + ) + }), +}) diff --git a/server/routers/contentstack/schemas/blocks/hotelFaq.ts b/server/routers/contentstack/schemas/blocks/hotelFaq.ts new file mode 100644 index 000000000..b6d4d42cf --- /dev/null +++ b/server/routers/contentstack/schemas/blocks/hotelFaq.ts @@ -0,0 +1,75 @@ +import { z } from "zod" + +import { + accordionItemsSchema, + globalAccordionConnectionRefs, + specificAccordionConnectionRefs, +} from "./accordion" + +import { BlocksEnums } from "@/types/enums/blocks" +import { HotelPageEnum } from "@/types/enums/hotelPage" + +export const hotelFaqSchema = z + .object({ + typename: z + .literal(BlocksEnums.block.Accordion) + .optional() + .default(BlocksEnums.block.Accordion), + title: z.string().optional().default(""), + global_faqConnection: z + .object({ + edges: z.array( + z.object({ + node: z.object({ + questions: accordionItemsSchema, + }), + }) + ), + }) + .optional(), + specific_faq: z + .object({ + questions: accordionItemsSchema, + }) + .optional(), + }) + .transform((data) => { + const array = [] + array.push( + data.global_faqConnection?.edges.flatMap(({ node: faqConnection }) => { + return faqConnection.questions + }) || [] + ) + array.push(data.specific_faq?.questions || []) + return { ...data, accordions: array.flat(2) } + }) + +export const hotelFaqRefsSchema = z + .object({ + __typename: z + .literal(HotelPageEnum.ContentStack.blocks.Faq) + .optional() + .default(HotelPageEnum.ContentStack.blocks.Faq), + global_faqConnection: globalAccordionConnectionRefs.optional(), + specific_faq: specificAccordionConnectionRefs.optional(), + }) + .transform((data) => { + const array = [] + array.push( + data.global_faqConnection?.edges.flatMap(({ node: faqConnection }) => { + return faqConnection.questions.flatMap((question) => + question.answer.embedded_itemsConnection.edges.flatMap( + ({ node }) => node.system + ) + ) + }) || [] + ) + array.push( + data.specific_faq?.questions.flatMap((question) => + question.answer.embedded_itemsConnection.edges.flatMap( + ({ node }) => node.system + ) + ) || [] + ) + return array.flat(2) + }) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 198219e0e..1839f26a5 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,5 +1,6 @@ import { metrics } from "@opentelemetry/api" +import { Lang } from "@/constants/languages" import * as api from "@/lib/api" import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql" import { request } from "@/lib/graphql/request" @@ -18,6 +19,12 @@ import { import { toApiLang } from "@/server/utils" import { hotelPageSchema } from "../contentstack/hotelPage/output" +import { + fetchHotelPageRefs, + generatePageTags, + getHotelPageCounter, + validateHotelPageRefs, +} from "../contentstack/hotelPage/utils" import { getHotelInputSchema, getHotelsAvailabilityInputSchema, @@ -70,14 +77,38 @@ const roomsAvailabilityFailCounter = meter.createCounter( "trpc.hotel.availability.rooms-fail" ) -async function getContentstackData( - locale: string, - uid: string | null | undefined -) { - const response = await request(GetHotelPage, { - locale, - uid, - }) +async function getContentstackData(lang: Lang, uid?: string | null) { + if (!uid) { + return null + } + const contentPageRefsData = await fetchHotelPageRefs(lang, uid) + const contentPageRefs = validateHotelPageRefs(contentPageRefsData, lang, uid) + if (!contentPageRefs) { + return null + } + + const tags = generatePageTags(contentPageRefs, lang) + + getHotelPageCounter.add(1, { lang, uid }) + console.info( + "contentstack.hotelPage start", + JSON.stringify({ + query: { lang, uid }, + }) + ) + const response = await request( + GetHotelPage, + { + locale: lang, + uid, + }, + { + cache: "force-cache", + next: { + tags, + }, + } + ) if (!response.data) { throw notFound(response) @@ -86,7 +117,7 @@ async function getContentstackData( const hotelPageData = hotelPageSchema.safeParse(response.data) if (!hotelPageData.success) { console.error( - `Failed to validate Hotel Page - (uid: ${uid}, lang: ${locale})` + `Failed to validate Hotel Page - (uid: ${uid}, lang: ${lang})` ) console.error(hotelPageData.error) return null @@ -101,6 +132,7 @@ export const hotelQueryRouter = router({ .query(async ({ ctx, input }) => { const { lang, uid } = ctx const { include } = input + const contentstackData = await getContentstackData(lang, uid) const hotelId = contentstackData?.hotel_page_id @@ -230,6 +262,7 @@ export const hotelQueryRouter = router({ roomCategories, activitiesCard: activities?.upcoming_activities_card, facilities, + faq: contentstackData?.faq, } }), availability: router({ diff --git a/server/routers/user/utils.ts b/server/routers/user/utils.ts index 7196f389d..cde50270f 100644 --- a/server/routers/user/utils.ts +++ b/server/routers/user/utils.ts @@ -1,4 +1,5 @@ import { metrics } from "@opentelemetry/api" + import { Lang } from "@/constants/languages" import { env } from "@/env/server" import * as api from "@/lib/api" @@ -9,7 +10,9 @@ import type { FriendTransaction, Stay } from "./output" const meter = metrics.getMeter("trpc.user") const getProfileCounter = meter.createCounter("trpc.user.profile") -const getProfileSuccessCounter = meter.createCounter("trpc.user.profile-success") +const getProfileSuccessCounter = meter.createCounter( + "trpc.user.profile-success" +) const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail") async function updateStaysBookingUrl( @@ -41,7 +44,7 @@ async function updateStaysBookingUrl( // Temporary Url, domain and lang support for current web const bookingUrl = new URL( "/hotelreservation/my-booking", - env.PUBLIC_URL ?? "" + env.PUBLIC_URL || "https://www.scandichotels.com" // fallback to production for ephemeral envs (like deploy previews) ) switch (lang) { case Lang.sv: @@ -83,10 +86,7 @@ async function updateStaysBookingUrl( } getProfileSuccessCounter.add(1) - console.info( - "api.user.profile updatebookingurl success", - JSON.stringify({}) - ) + console.info("api.user.profile updatebookingurl success", JSON.stringify({})) return data.map((d) => { const originalString = @@ -98,10 +98,7 @@ async function updateStaysBookingUrl( bookingUrl.searchParams.set("RefId", encryptedBookingValue) } else { bookingUrl.searchParams.set("lastName", apiJson.data.attributes.lastName) - bookingUrl.searchParams.set( - "bookingId", - d.attributes.confirmationNumber - ) + bookingUrl.searchParams.set("bookingId", d.attributes.confirmationNumber) } return { ...d, @@ -113,4 +110,4 @@ async function updateStaysBookingUrl( }) } -export { updateStaysBookingUrl } \ No newline at end of file +export { updateStaysBookingUrl } diff --git a/server/utils.ts b/server/utils.ts index 0179ddf93..6d6251763 100644 --- a/server/utils.ts +++ b/server/utils.ts @@ -1,6 +1,8 @@ +import { NextRequest } from "next/server" import { z } from "zod" import { Lang } from "@/constants/languages" +import { env } from "@/env/server" export const langInput = z.object({ lang: z.nativeEnum(Lang), @@ -33,3 +35,93 @@ export function toLang(lang: string): Lang | undefined { const lowerCaseLang = lang.toLowerCase() return Object.values(Lang).find((l) => l === lowerCaseLang) } + +/** + * Use this function when you want to create URLs that are public facing, for + * example for redirects or redirectTo query parameters. + * Dedicated environments are behind Akamai (test, stage, production). They have + * env.PUBLIC_URL set. + * All other environment like deploy previews and branch deployments are not + * behind Akamai and therefore do not have env.PUBLIC_URL set. + * We need this approach because Netlify uses x-forwarded-host internally and + * strips it from ever reaching our code. + * TODO: Replace this approach with custom header in Akamai that mirrors the + * value in x-forwarded-host which would not get stripped by Netlify. + * @param request The incoming request. + * @returns NextURL The public facing URL instance for the given request. + */ +export function getPublicNextURL(request: NextRequest) { + if (env.PUBLIC_URL) { + const publicNextURL = request.nextUrl.clone() + // Akamai in front of Netlify for dedicated environments + // require us to rewrite the incoming host and hostname + // to match the public URL used to visit Akamai. + const url = new URL(env.PUBLIC_URL) + publicNextURL.host = url.host + publicNextURL.hostname = url.hostname + return publicNextURL + } + return request.nextUrl +} + +/** + * Use this function when you want the public facing URL for the given request. + * Read about the motivation in getPublicNextURL above. + * @see getPublicNextURL + * @param request The incoming request. + * @returns string The public facing origin for the given request. + */ +export function getPublicURL(request: NextRequest) { + if (env.PUBLIC_URL) { + return env.PUBLIC_URL + } + return request.nextUrl.origin +} + +/** + * Use this function when you want to create URLs that are internal (behind Akamai), + * for example for rewrites. Mainly used for middleware wrapped in auth(). + * The auth() function overrides the origin of the incoming request. It sets it + * to the origin of AUTH_URL/NEXTAUTH_URL. This means we cannot use the augmented + * request from auth() for rewrites, as those will point to auth url origin + * (in front of Akamai) and not the origin of the incoming request (behind Akamai). + * This results in rewrites going over the internet instead of going through the + * internal routing at Netlify. + * For dedicated environments (test, stage and production) we are behind Akamai. + * For those we have set a value for AUTH_URL/NEXTAUTH_URL, they point to the + * PUBLIC_URL. For rewrites we need the internal origin inside Netlify. + * In middleware.ts we copy the incoming origin to a header 'x-sh-origin'. We try + * and use that first, if not present we assume the internal origin is the value + * of the host header, as that is what Netlify used for routing to this deployment. + * @param request The incoming request. + * @returns NextURL The internal request, in Netlify behind Akamai. + */ +export function getInternalNextURL(request: NextRequest) { + const { href, origin } = request.nextUrl + + const originHeader = request.headers.get("x-sh-origin") + if (originHeader) { + console.log(`[internalNextUrl] using x-sh-origin header`, { + origin, + originHeader, + newOrigin: href.replace(origin, originHeader), + }) + return new NextRequest(href.replace(origin, originHeader), request).nextUrl + } + + const hostHeader = request.headers.get("host") + if (hostHeader) { + const inputHostOrigin = `${request.nextUrl.protocol}//${hostHeader}` + console.log(`[internalNextUrl] using host header`, { + origin, + hostHeader, + hostOrigin: inputHostOrigin, + newOrigin: href.replace(origin, inputHostOrigin), + }) + const { origin: hostOrigin } = new URL(inputHostOrigin) + return new NextRequest(href.replace(origin, hostOrigin), request).nextUrl + } + + console.log(`[internalNextUrl] falling back to incoming request`) + return request.nextUrl +} diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 41d15c3a5..16dd96f96 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -7,6 +7,7 @@ import { breakfastSchema } from "@/components/HotelReservation/EnterDetails/Brea import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" import { DetailsSchema } from "@/types/components/enterDetails/details" +import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { StepEnum } from "@/types/components/enterDetails/step" import { bedTypeEnum } from "@/types/enums/bedType" import { breakfastEnum } from "@/types/enums/breakfast" @@ -18,9 +19,15 @@ interface EnterDetailsState { } & DetailsSchema steps: StepEnum[] currentStep: StepEnum + activeSidePeek: SidePeekEnum | null isValid: Record completeStep: (updatedData: Partial) => void - navigate: (step: StepEnum, searchParams?: Record) => void + navigate: ( + step: StepEnum, + searchParams?: Record + ) => void + openSidePeek: (key: SidePeekEnum | null) => void + closeSidePeek: () => void } export function initEditDetailsState(currentStep: StepEnum) { @@ -33,26 +40,34 @@ export function initEditDetailsState(currentStep: StepEnum) { breakfast: undefined, countryCode: "", email: "", - firstname: "", - lastname: "", + firstName: "", + lastName: "", phoneNumber: "", + join: false, + zipCode: "", + dateOfBirth: undefined, + termsAccepted: false, } let inputData = {} if (search?.size) { - const searchParams: Record = {} + const searchParams: Record = {} search.forEach((value, key) => { - searchParams[key] = value + // Handle boolean values + + if (value === "true" || value === "false") { + searchParams[key] = JSON.parse(value) as true | false + } else { + searchParams[key] = value + } }) inputData = searchParams - } else if (sessionData) { - inputData = JSON.parse(sessionData) } const validPaths = [StepEnum.selectBed] - let initialData = defaultData + let initialData: EnterDetailsState["data"] = defaultData const isValid = { [StepEnum.selectBed]: false, @@ -96,7 +111,7 @@ export function initEditDetailsState(currentStep: StepEnum) { const query = new URLSearchParams(window.location.search) if (searchParams) { Object.entries(searchParams).forEach(([key, value]) => { - query.set(key, value) + query.set(key, value ? value.toString() : "") }) } @@ -104,7 +119,10 @@ export function initEditDetailsState(currentStep: StepEnum) { window.history.pushState({}, "", step + "?" + query.toString()) }) ), + openSidePeek: (key) => set({ activeSidePeek: key }), + closeSidePeek: () => set({ activeSidePeek: null }), currentStep, + activeSidePeek: null, isValid, completeStep: (updatedData) => set( diff --git a/stores/guests-rooms.ts b/stores/guests-rooms.ts new file mode 100644 index 000000000..2866184b0 --- /dev/null +++ b/stores/guests-rooms.ts @@ -0,0 +1,144 @@ +"use client" + +import { produce } from "immer" +import { create } from "zustand" + +import { BedTypeEnum } from "@/types/components/bookingWidget/enums" +import { Child } from "@/types/components/bookingWidget/guestsRoomsPicker" + +interface GuestsRooms { + rooms: [ + { + adults: number + children: Child[] + childrenInAdultsBed: number + }, + ] + adultCount: number + childCount: number + isValidated: boolean + increaseAdults: (roomIndex: number) => void + decreaseAdults: (roomIndex: number) => void + increaseChildren: (roomIndex: number) => void + decreaseChildren: (roomIndex: number) => Child[] + updateChildAge: (age: number, roomIndex: number, childIndex: number) => void + updateChildBed: (bed: number, roomIndex: number, childIndex: number) => void + increaseChildInAdultsBed: (roomIndex: number) => void + decreaseChildInAdultsBed: (roomIndex: number) => void + increaseRoom: () => void + decreaseRoom: (roomIndex: number) => void + setIsValidated: (isValidated: boolean) => void +} + +export const useGuestsRoomsStore = create((set, get) => ({ + rooms: [ + { + adults: 1, + children: [], + childrenInAdultsBed: 0, + }, + ], + adultCount: 1, + childCount: 0, + isValidated: false, + increaseAdults: (roomIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].adults = state.rooms[roomIndex].adults + 1 + state.adultCount = state.adultCount + 1 + }) + ), + decreaseAdults: (roomIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].adults = state.rooms[roomIndex].adults - 1 + state.adultCount = state.adultCount - 1 + if ( + state.rooms[roomIndex].childrenInAdultsBed > + state.rooms[roomIndex].adults + ) { + const toUpdateIndex = state.rooms[roomIndex].children.findIndex( + (child) => child.bed == BedTypeEnum.IN_ADULTS_BED + ) + if (toUpdateIndex != -1) { + state.rooms[roomIndex].children[toUpdateIndex].bed = + state.rooms[roomIndex].children[toUpdateIndex].age < 3 + ? BedTypeEnum.IN_CRIB + : BedTypeEnum.IN_EXTRA_BED + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].adults + } + } + }) + ), + increaseChildren: (roomIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].children.push({ + age: -1, + bed: -1, + }) + state.childCount = state.childCount + 1 + }) + ), + decreaseChildren: (roomIndex) => { + set( + produce((state: GuestsRooms) => { + const roomChildren = state.rooms[roomIndex].children + if ( + roomChildren.length && + roomChildren[roomChildren.length - 1].bed == BedTypeEnum.IN_ADULTS_BED + ) { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed - 1 + } + state.rooms[roomIndex].children.pop() + state.childCount = state.childCount - 1 + }) + ) + return get().rooms[roomIndex].children + }, + updateChildAge: (age, roomIndex, childIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].children[childIndex].age = age + }) + ), + updateChildBed: (bed, roomIndex, childIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].children[childIndex].bed = bed + }) + ), + increaseChildInAdultsBed: (roomIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed + 1 + }) + ), + decreaseChildInAdultsBed: (roomIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms[roomIndex].childrenInAdultsBed = + state.rooms[roomIndex].childrenInAdultsBed - 1 + }) + ), + increaseRoom: () => + set( + produce((state: GuestsRooms) => { + state.rooms.push({ + adults: 1, + children: [], + childrenInAdultsBed: 0, + }) + }) + ), + decreaseRoom: (roomIndex) => + set( + produce((state: GuestsRooms) => { + state.rooms.splice(roomIndex, 1) + }) + ), + setIsValidated: (isValidated) => set(() => ({ isValidated })), +})) diff --git a/types/components/blocks/Accordion.ts b/types/components/blocks/Accordion.ts new file mode 100644 index 000000000..669b28b1d --- /dev/null +++ b/types/components/blocks/Accordion.ts @@ -0,0 +1,6 @@ +import type { Accordion } from "@/server/routers/contentstack/schemas/blocks/accordion" + +export type AccordionProps = { + accordion: Accordion["accordion"]["accordions"] + title?: string +} diff --git a/types/components/bookingWidget/enums.ts b/types/components/bookingWidget/enums.ts new file mode 100644 index 000000000..ae63ad571 --- /dev/null +++ b/types/components/bookingWidget/enums.ts @@ -0,0 +1,5 @@ +export enum BedTypeEnum { + IN_ADULTS_BED = 0, + IN_CRIB = 1, + IN_EXTRA_BED = 2, +} diff --git a/types/components/bookingWidget/guestsRoomsPicker.ts b/types/components/bookingWidget/guestsRoomsPicker.ts new file mode 100644 index 000000000..5b3b7b3e2 --- /dev/null +++ b/types/components/bookingWidget/guestsRoomsPicker.ts @@ -0,0 +1,44 @@ +export type ChildBed = { + label: string + value: number +} + +export type Child = { + age: number + bed: number +} + +export type GuestsRoom = { + adults: number + children: Child[] +} + +export interface GuestsRoomsPickerProps { + closePicker: () => void +} + +export type GuestsRoomPickerProps = { + index: number +} + +export type AdultSelectorProps = { + roomIndex: number +} + +export type ChildSelectorProps = { + roomIndex: number +} + +export type ChildInfoSelectorProps = { + child: Child + index: number + roomIndex: number +} + +export interface CounterProps { + count: number + handleOnIncrease: () => void + handleOnDecrease: () => void + disableIncrease: boolean + disableDecrease: boolean +} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.ts b/types/components/checkbox/index.ts similarity index 100% rename from components/TempDesignSystem/Form/Checkbox/checkbox.ts rename to types/components/checkbox/index.ts diff --git a/types/components/datepicker.ts b/types/components/datepicker.ts index 1d30aa3ae..999a8ef1d 100644 --- a/types/components/datepicker.ts +++ b/types/components/datepicker.ts @@ -11,7 +11,7 @@ type LangWithoutEn = Lang.da | Lang.de | Lang.fi | Lang.no | Lang.sv export interface DatePickerProps { close: () => void - handleOnSelect: (selected: DateRange) => void + handleOnSelect: (selected: Date) => void locales: Record selectedDate: DateRange } diff --git a/types/components/enterDetails/details.ts b/types/components/enterDetails/details.ts index dbd2ecb7e..55e6864f0 100644 --- a/types/components/enterDetails/details.ts +++ b/types/components/enterDetails/details.ts @@ -4,7 +4,7 @@ import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Detail import type { SafeUser } from "@/types/user" -export interface DetailsSchema extends z.output {} +export type DetailsSchema = z.output export interface DetailsProps { user: SafeUser diff --git a/types/components/enterDetails/sidePeek.ts b/types/components/enterDetails/sidePeek.ts new file mode 100644 index 000000000..b109452c6 --- /dev/null +++ b/types/components/enterDetails/sidePeek.ts @@ -0,0 +1,9 @@ +import { Hotel } from "@/types/hotel" + +export enum SidePeekEnum { + hotelDetails = "hotel-detail-side-peek", +} + +export type SidePeekProps = { + hotel: Hotel +} diff --git a/types/components/hotelPage/roomCard.ts b/types/components/hotelPage/room.ts similarity index 79% rename from types/components/hotelPage/roomCard.ts rename to types/components/hotelPage/room.ts index 425edc128..cfa3593c9 100644 --- a/types/components/hotelPage/roomCard.ts +++ b/types/components/hotelPage/room.ts @@ -7,3 +7,7 @@ export interface RoomCardProps { subtitle: string badgeTextTransKey: string | null } + +export type RoomsProps = { + rooms: RoomData[] +} diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index 58112ac16..473de563b 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -2,6 +2,8 @@ import { z } from "zod" import { Product, productTypePriceSchema } from "@/server/routers/hotels/output" +import { Rate } from "./selectRate" + type ProductPrice = z.output export type FlexibilityOptionProps = { @@ -10,6 +12,8 @@ export type FlexibilityOptionProps = { value: string paymentTerm: string priceInformation?: Array + roomType: string + handleSelectRate: (rate: Rate) => void } export interface PriceListProps { diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts new file mode 100644 index 000000000..672df21dd --- /dev/null +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -0,0 +1,6 @@ +import { Rate } from "./selectRate" + +export interface RateSummaryProps { + rateSummary: Rate + isUserLoggedIn: boolean +} diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 834c9dea3..a6ed91ac1 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -3,10 +3,13 @@ import { RoomConfiguration, } from "@/server/routers/hotels/output" +import { Rate } from "./selectRate" + import { RoomData } from "@/types/hotel" export type RoomCardProps = { roomConfiguration: RoomConfiguration rateDefinitions: RateDefinition[] roomCategories: RoomData[] + handleSelectRate: (rate: Rate) => void } diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 16c26fd90..9e944f5d8 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,8 +1,10 @@ import { RoomsAvailability } from "@/server/routers/hotels/output" import { RoomData } from "@/types/hotel" +import { SafeUser } from "@/types/user" export interface RoomSelectionProps { roomConfigurations: RoomsAvailability roomCategories: RoomData[] + user: SafeUser } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index bc325bbf2..43152f11b 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -1,5 +1,14 @@ +import { Product } from "@/server/routers/hotels/output" + export interface SelectRateSearchParams { fromDate: string toDate: string hotel: string } + +export interface Rate { + roomType: string + priceName: string + public: Product["productType"]["public"] + member: Product["productType"]["member"] +} diff --git a/types/components/myPages/header.ts b/types/components/myPages/header.ts index 619dc805b..82a662a94 100644 --- a/types/components/myPages/header.ts +++ b/types/components/myPages/header.ts @@ -1,11 +1,11 @@ -import { HeadingProps } from "@/components/TempDesignSystem/Text/Title/title" +import type { HeadingProps } from "@/components/TempDesignSystem/Text/Title/title" export type HeaderProps = { link?: { href: string text: string } - preamble: string | null + preamble?: string | null textTransform?: HeadingProps["textTransform"] title: string | null topTitle?: boolean diff --git a/types/enums/blocks.ts b/types/enums/blocks.ts index 81a088bd1..83ee7a13e 100644 --- a/types/enums/blocks.ts +++ b/types/enums/blocks.ts @@ -1,5 +1,6 @@ export namespace BlocksEnums { export const enum block { + Accordion = "Accordion", CardsGrid = "CardsGrid", Content = "Content", DynamicContent = "DynamicContent", diff --git a/types/enums/cardsGrid.ts b/types/enums/cardsGrid.ts index 9fa1d560a..314a26926 100644 --- a/types/enums/cardsGrid.ts +++ b/types/enums/cardsGrid.ts @@ -7,6 +7,7 @@ export namespace CardsGridEnum { } export enum CardsGridLayoutEnum { + ONE_COLUMN = "oneColumnGrid", TWO_COLUMNS = "twoColumnGrid", THREE_COLUMNS = "threeColumnGrid", TWO_PLUS_ONE = "twoPlusOne", // Not sure if this is used? diff --git a/types/enums/contentPage.ts b/types/enums/contentPage.ts index df5dab28e..16366a756 100644 --- a/types/enums/contentPage.ts +++ b/types/enums/contentPage.ts @@ -1,6 +1,7 @@ export namespace ContentPageEnum { export namespace ContentStack { export const enum blocks { + Accordion = "ContentPageBlocksAccordion", CardsGrid = "ContentPageBlocksCardsGrid", Content = "ContentPageBlocksContent", DynamicContent = "ContentPageBlocksDynamicContent", diff --git a/types/enums/hotelPage.ts b/types/enums/hotelPage.ts index c7de7b727..bfae4bfbe 100644 --- a/types/enums/hotelPage.ts +++ b/types/enums/hotelPage.ts @@ -1,6 +1,7 @@ export namespace HotelPageEnum { export namespace ContentStack { export const enum blocks { + Faq = "HotelPageFaq", ActivitiesCard = "HotelPageContentUpcomingActivitiesCard", } } diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts index 6bc343278..e3f0f99bb 100644 --- a/types/trpc/routers/contentstack/contentPage.ts +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -4,6 +4,7 @@ import { blocksSchema, contentPageRefsSchema, contentPageSchema, + contentPageSchemaBlocks, sidebarSchema, } from "@/server/routers/contentstack/contentPage/output" diff --git a/types/trpc/routers/contentstack/hotelPage.ts b/types/trpc/routers/contentstack/hotelPage.ts index 329a7b207..e157eaecf 100644 --- a/types/trpc/routers/contentstack/hotelPage.ts +++ b/types/trpc/routers/contentstack/hotelPage.ts @@ -2,14 +2,19 @@ import { z } from "zod" import { contentBlock, + hotelPageRefsSchema, hotelPageSchema, } from "@/server/routers/contentstack/hotelPage/output" -import { activitiesCard } from "@/server/routers/contentstack/schemas/blocks/activitiesCard" +import { activitiesCardSchema } from "@/server/routers/contentstack/schemas/blocks/activitiesCard" -// Will be extended once we introduce more functionality to our entries. export interface GetHotelPageData extends z.input {} export interface HotelPage extends z.output {} -export interface ActivitiesCard extends z.output {} +export interface ActivitiesCard extends z.output {} export type ActivityCard = ActivitiesCard["upcoming_activities_card"] export interface ContentBlock extends z.output {} + +export interface GetHotelPageRefsSchema + extends z.input {} + +export interface HotelPageRefs extends z.output {} diff --git a/update-dotenv.mjs b/update-dotenv.mjs deleted file mode 100644 index 6f4521b78..000000000 --- a/update-dotenv.mjs +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Build time environment variables are not available in Netlify functions at - * runtime. The official workaround is to create an artifact and include that - * in the bundled functions so that the function can load/read it at runtime. - * In other words, during the build fill the .env file with the environment - * variables needed and then instruct Netlify to include the .env file together - * with the bundled function. - * - * This works but has two things to consider: - * - * 1. Any environment variable created in the Netlify UI will be considered - * defined. Even if the variable is set to "empty" in the Netlify UI it is - * still an empty string and therefore defined. - * - * 2. Next.js uses @next/env to automatically read the .env - * file into the process environment. @next/env does NOT override any - * defined variables, empty strings are also considered defined. So for - * @next/env to automatically pick up the .env file in the bundled functions - * we need to make sure that none of the variables in the .env file are - * defined in the Netlify UI. @next/env does not have any "override=true" - * option, like dotenv package. So rather than introduce dotenv and manually - * use it in *every* function, we can delegate to @next/env if we keep the - * environment variables in Netlify UI in check. - * - * We only run this on Netlify build. - * - * We define PUBLIC_URL and use that because we are behind Akamai reverse proxy. - * For the stable environments (test, stage, production) these are defined. For - * any other environment (branch deploys and deploy previews) we use the - * predefined Netlify environment variable DEPLOY_PRIME_URL. - * - * Both AUTH_URL and NEXTAUTH_URL is set to point to the PUBLIC_URL. - * We set both as a precaution as next-auth v5 is transitioning to AUTH_* but we - * have seen several occurences in the auth.js codebase that are not using both. - */ - -import fs from "node:fs" - -if (process.env.NETLIFY) { - const PUBLIC_URLS = { - production: "https://www.scandichotels.com", - stage: "https://stage.scandichotels.com", - test: "https://test2.scandichotels.com", - } - - let PUBLIC_URL - if (PUBLIC_URLS[process.env.CONTEXT]) { - PUBLIC_URL = PUBLIC_URLS[process.env.CONTEXT] - } else if (PUBLIC_URLS[process.env.BRANCH]) { - PUBLIC_URL = PUBLIC_URLS[process.env.BRANCH] - } else { - PUBLIC_URL = process.env.DEPLOY_PRIME_URL - } - - const AUTH_URL = `${PUBLIC_URL}/api/web/auth` - const NEXTAUTH_URL = AUTH_URL - - const replaceMap = { - AUTH_URL, - NEXTAUTH_URL, - PUBLIC_URL, - } - - let contents = fs.readFileSync("./.env", { encoding: "utf-8" }) - - for (const [key, value] of Object.entries(replaceMap)) { - contents = contents.replace(new RegExp(`${key}=.*`), `${key}="${value}"`) - } - - fs.writeFileSync("./.env", contents, { encoding: "utf-8" }) -}