diff --git a/.env.test b/.env.test index 8d70bab9d..801c4336b 100644 --- a/.env.test +++ b/.env.test @@ -12,6 +12,7 @@ CURITY_CLIENT_SECRET_SERVICE="test" CURITY_CLIENT_ID_USER="test" CURITY_CLIENT_SECRET_USER="test" CURITY_ISSUER_USER="test" +CURITY_ISSUER_SERVICE="test" CYPRESS_API_BASEURL="test" CYPRESS_CURITY_USERNAME="test" CYPRESS_CURITY_PASSWORD="test" @@ -35,3 +36,4 @@ SEAMLESS_LOGOUT_FI="test" SEAMLESS_LOGOUT_NO="test" SEAMLESS_LOGOUT_SV="test" WEBVIEW_ENCRYPTION_KEY="test" +BOOKING_ENCRYPTION_KEY="test" diff --git a/app/[lang]/(live)/(protected)/layout.tsx b/app/[lang]/(live)/(protected)/layout.tsx index 024071af3..19ad58daf 100644 --- a/app/[lang]/(live)/(protected)/layout.tsx +++ b/app/[lang]/(live)/(protected)/layout.tsx @@ -20,13 +20,17 @@ export default async function ProtectedLayout({ h.get("x-url") ?? h.get("x-pathname") ?? overview[getLang()] ) + const redirectURL = `/${getLang()}/login?redirectTo=${redirectTo}` + if (!session) { - redirect(`/${getLang()}/login?redirectTo=${redirectTo}`) + console.log(`[layout:protected] no session, redirecting to: ${redirectURL}`) + redirect(redirectURL) } const user = await serverClient().user.get() if (!user || "error" in user) { - redirect(`/${getLang()}/login?redirectTo=${redirectTo}`) + console.log(`[layout:protected] no user, redirecting to: ${redirectURL}`) + redirect(redirectURL) } return children diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index 38b98361a..2bb2f0a52 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -1,5 +1,3 @@ -import { createActionURL } from "@auth/core" -import { headers as nextHeaders } from "next/headers" import { NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" @@ -16,11 +14,35 @@ export async function GET( let redirectTo: string = "" const returnUrl = request.headers.get("x-returnurl") + const isSeamless = request.headers.get("x-logout-source") === "seamless" - if (returnUrl) { - // Seamless logout request from Current web - redirectTo = returnUrl + console.log( + `[logout] source: ${request.headers.get("x-logout-source") || "normal"}` + ) + + const redirectToSearchParamValue = + request.nextUrl.searchParams.get("redirectTo") + const redirectToFallback = "/" + + if (isSeamless) { + if (returnUrl) { + redirectTo = returnUrl + } else { + console.log( + `[login] missing returnUrl, using fallback: ${redirectToFallback}` + ) + redirectTo = redirectToFallback + } } else { + redirectTo = redirectToSearchParamValue || redirectToFallback + + // 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 + console.log(`[logout] make redirectTo absolute, to ${redirectTo}`) + } + try { // Initiate the seamless logout flow let redirectUrlValue @@ -45,6 +67,9 @@ export async function GET( break } const redirectUrl = new URL(redirectUrlValue) + console.log( + `[logout] creating redirect to seamless logout: ${redirectUrl}` + ) redirectTo = redirectUrl.toString() } catch (e) { console.error( @@ -55,37 +80,25 @@ export async function GET( } try { + redirectTo = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}` + console.log(`[logout] final redirectUrl: ${redirectTo}`) + console.log({ logout_env: process.env }) + /** * Passing `redirect: false` to `signOut` will return a result object * instead of automatically redirecting inside of `signOut`. * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L104 */ - console.log({ logout_NEXTAUTH_URL: process.env.NEXTAUTH_URL }) - console.log({ logout_env: process.env }) - - const headers = new Headers(nextHeaders()) - const signOutURL = createActionURL( - "signout", - // @ts-expect-error `x-forwarded-proto` is not nullable, next.js sets it by default - headers.get("x-forwarded-proto"), - headers, - process.env - ) - - console.log({ logout_signOutURL: signOutURL }) - - // Redirect to Curity logout - const curityLogoutUrl = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}` - - console.log({ logout_redirectTo: curityLogoutUrl }) - const redirectUrlObj = await signOut({ - redirectTo: curityLogoutUrl, + redirectTo, redirect: false, }) if (redirectUrlObj) { + console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`) return NextResponse.redirect(redirectUrlObj.redirect) + } else { + console.error(`[logout] missing redirectUrlObj reponse from signOut()`) } } catch (error) { if (error instanceof AuthError) { diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx index b35861357..012c40de2 100644 --- a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx @@ -34,7 +34,6 @@ export default async function MyPages({

{formatMessage({ id: "No content published" })}

)} - ) diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css index 32b75c7b8..9f19f6eac 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.module.css @@ -14,17 +14,6 @@ gap: var(--Spacing-x1); } -.card { - display: grid; - align-items: center; - column-gap: var(--Spacing-x1); - grid-template-columns: auto auto auto 1fr; - justify-items: flex-end; - padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,); - border-radius: var(--Corner-radius-Small); - background-color: var(--Base-Background-Primary-Normal); -} - @media screen and (min-width: 768px) { .container { gap: var(--Spacing-x3); diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx index a3cff5783..400f10f2c 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx @@ -1,11 +1,9 @@ import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" -import { CreditCard, Delete } from "@/components/Icons" import AddCreditCardButton from "@/components/Profile/AddCreditCardButton" -import Button from "@/components/TempDesignSystem/Button" +import CreditCardList from "@/components/Profile/CreditCardList" import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -33,40 +31,8 @@ export default async function CreditCardSlot({ params }: PageArgs) { })} - {creditCards?.length ? ( -
- {creditCards.map((card, idx) => ( - - ))} -
- ) : null} - + + ) } - -function CreditCardRow({ - truncatedNumber, - cardType, -}: { - truncatedNumber: string - cardType: string -}) { - const maskedCardNumber = `**** ${truncatedNumber.slice(12, 16)}` - return ( -
- - {cardType} - {maskedCardNumber} - -
- ) -} diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index 73496fe4e..cdf46a35d 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -15,7 +15,8 @@ export default function ProfileLayout({ {profile} {creditCards} - {communication} + {/* TODO: Implement communication preferences flow. Hidden until decided on where to send user. */} + {/* {communication} */} ) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 0e8e98a05..6d2d5ffc6 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -1,8 +1,8 @@ import { notFound } from "next/navigation" import ContentPage from "@/components/ContentType/ContentPage" -import HotelPage from "@/components/ContentType/HotelPage/HotelPage" -import LoyaltyPage from "@/components/ContentType/LoyaltyPage/LoyaltyPage" +import HotelPage from "@/components/ContentType/HotelPage" +import LoyaltyPage from "@/components/ContentType/LoyaltyPage" import { setLang } from "@/i18n/serverContext" import { diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css new file mode 100644 index 000000000..35fc821c4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css @@ -0,0 +1,20 @@ +.main { + display: flex; + justify-content: center; + padding: var(--Spacing-x4); + background-color: var(--Scandic-Brand-Warm-White); + min-height: 100dvh; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); + width: 100%; + max-width: 365px; +} +@media screen and (min-width: 1367px) { + .section { + max-width: 525px; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx new file mode 100644 index 000000000..46f941bbc --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.tsx @@ -0,0 +1,20 @@ +import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection" +import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection" +import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection" +import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData" + +import styles from "./page.module.css" + +export default function BookingConfirmationPage() { + const { email, hotel, stay, summary } = tempConfirmationData + + return ( +
+
+ + + +
+
+ ) +} diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts index ba8292d89..702282d22 100644 --- a/app/[lang]/(live)/(public)/login/route.ts +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -11,32 +11,51 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - let redirectHeaders: Headers | undefined = undefined - let redirectTo: string - - const returnUrl = request.headers.get("x-returnurl") - const isMFA = request.headers.get("x-mfa-login") - - // This is to support seamless login when using magic link login - const isMagicLinkUpdateLogin = !!request.headers.get("x-magic-link") - if (!env.PUBLIC_URL) { throw internalServerError("No value for env.PUBLIC_URL") } - if (returnUrl) { - // Seamless login request from Current web - redirectTo = returnUrl + let redirectHeaders: Headers | undefined = undefined + let redirectTo: string + + const returnUrl = request.headers.get("x-returnurl") + const isSeamless = request.headers.get("x-login-source") === "seamless" + const isMFA = request.headers.get("x-login-source") === "mfa" + const isSeamlessMagicLink = + request.headers.get("x-login-source") === "seamless-magiclink" + + console.log( + `[login] source: ${request.headers.get("x-login-source") || "normal"}` + ) + + const redirectToCookieValue = request.cookies.get("redirectTo")?.value // Cookie gets set by authRequired middleware + const redirectToSearchParamValue = + request.nextUrl.searchParams.get("redirectTo") + const redirectToFallback = "/" + + console.log(`[login] redirectTo cookie value: ${redirectToCookieValue}`) + console.log( + `[login] redirectTo search param value: ${redirectToSearchParamValue}` + ) + + if (isSeamless || isSeamlessMagicLink) { + if (returnUrl) { + redirectTo = returnUrl + } else { + console.log( + `[login] missing returnUrl, using fallback: ${redirectToFallback}` + ) + redirectTo = redirectToFallback + } } else { - // Normal login request from New web redirectTo = - request.cookies.get("redirectTo")?.value || // Cookie gets set by authRequired middleware - request.nextUrl.searchParams.get("redirectTo") || - "/" + redirectToCookieValue || redirectToSearchParamValue || redirectToFallback // 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 + console.log(`[login] make redirectTo absolute, to ${redirectTo}`) } // Clean up cookie from authRequired middleware @@ -70,7 +89,11 @@ export async function GET( break } const redirectUrl = new URL(redirectUrlValue) + console.log(`[login] creating redirect to seamless login: ${redirectUrl}`) redirectUrl.searchParams.set("returnurl", redirectTo) + console.log( + `[login] returnurl for seamless login: ${redirectUrl.searchParams.get("returnurl")}` + ) redirectTo = redirectUrl.toString() /** Set cookie with redirect Url to appropriately redirect user when using magic link login */ @@ -82,25 +105,20 @@ export async function GET( ) } catch (e) { console.error( - "Unable to create URL for seamless login, proceeding without it." + "[login] unable to create URL for seamless login, proceeding without it.", + e ) - console.error(e) } } try { - /** - * Passing `redirect: false` to `signIn` will return the URL instead of - * automatically redirecting to it inside of `signIn`. - * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 - */ - console.log({ login_NEXTAUTH_URL: process.env.NEXTAUTH_URL }) + console.log(`[login] final redirectUrl: ${redirectTo}`) console.log({ login_env: process.env }) - console.log({ login_redirectTo: redirectTo }) - const params = { + /** Record is next-auth typings */ + const params: Record = { ui_locales: context.params.lang, - scope: ["openid", "profile"].join(" "), + scope: ["openid", "profile"], /** * The `acr_values` param is used to make Curity display the proper login * page for Scandic. Without the parameter Curity presents some choices @@ -117,18 +135,25 @@ export async function GET( // This is new param set for differentiate between the Magic link login of New web and current web version: "2", } + if (isMFA) { // Append profile_update scope for MFA - params.scope = params.scope + " profile_udpate" + params.scope.push("profile_update") /** * The below acr value is required as for New Web same Curity Client is used for MFA * while in current web it is being setup using different Curity Client */ params.acr_values = "urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web" - } else if (isMagicLinkUpdateLogin) { + } else if (isSeamlessMagicLink) { params.acr_values = "abc" } + params.scope = params.scope.join(" ") + /** + * Passing `redirect: false` to `signIn` will return the URL instead of + * automatically redirecting to it inside of `signIn`. + * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 + */ const redirectUrl = await signIn( "curity", { @@ -139,9 +164,13 @@ export async function GET( ) if (redirectUrl) { - return NextResponse.redirect(redirectUrl, { + const redirectOpts = { headers: redirectHeaders, - }) + } + console.log(`[login] redirecting to: ${redirectUrl}`, redirectOpts) + return NextResponse.redirect(redirectUrl, redirectOpts) + } else { + console.error(`[login] missing redirectUrl reponse from signIn()`) } } catch (error) { if (error instanceof AuthError) { diff --git a/app/[lang]/(live)/(public)/verifymagiclink/route.ts b/app/[lang]/(live)/(public)/verifymagiclink/route.ts index 4f2a1e86d..12c4b132f 100644 --- a/app/[lang]/(live)/(public)/verifymagiclink/route.ts +++ b/app/[lang]/(live)/(public)/verifymagiclink/route.ts @@ -12,39 +12,54 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - let redirectTo: string - - // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login - redirectTo = - request.cookies.get("magicLinkRedirectTo")?.value || - "/" + context.params.lang - if (!env.PUBLIC_URL) { throw internalServerError("No value for env.PUBLIC_URL") } + const loginKey = request.nextUrl.searchParams.get("loginKey") + if (!loginKey) { + console.log( + `[verifymagiclink] missing required loginKey, aborting bad request` + ) + return badRequest() + } + + let redirectTo: string + + console.log(`[verifymagiclink] verifying callback`) + + const redirectToCookieValue = request.cookies.get( + "magicLinkRedirectTo" + )?.value // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login + const redirectToFallback = "/" + + console.log( + `[verifymagiclink] magicLinkRedirectTo cookie value: ${redirectToCookieValue}` + ) + + redirectTo = redirectToCookieValue || redirectToFallback + // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { + console.log( + `[verifymagiclink] make redirectTo absolute, from ${redirectTo}` + ) redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`) } // Update Seamless login url as Magic link login has a different authenticator in Curity redirectTo = redirectTo.replace("updatelogin", "updateloginemail") - const loginKey = request.nextUrl.searchParams.get("loginKey") - - if (!loginKey) { - return badRequest() - } - try { + console.log(`[verifymagiclink] final redirectUrl: ${redirectTo}`) + /** * Passing `redirect: false` to `signIn` will return the URL instead of * automatically redirecting to it inside of `signIn`. * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 */ - console.log({ login_redirectTo: redirectTo }) - let redirectUrl = await signIn( + const redirectUrl = await signIn( "curity", { redirectTo, @@ -61,7 +76,12 @@ export async function GET( ) if (redirectUrl) { + console.log(`[verifymagiclink] redirecting to: ${redirectUrl}`) return NextResponse.redirect(redirectUrl) + } else { + console.error( + `[verifymagiclink] missing redirectUrl reponse from signIn()` + ) } } catch (error) { if (error instanceof AuthError) { diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 83ccc015a..35f37d653 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -15,14 +15,9 @@ import { getIntl } from "@/i18n" import ServerIntlProvider from "@/i18n/Provider" import { getLang, setLang } from "@/i18n/serverContext" -import type { Metadata } from "next" - import type { LangParams, LayoutArgs } from "@/types/params" -export const metadata: Metadata = { - description: "New web", - title: "Scandic Hotels", -} +export { generateMetadata } from "@/utils/generateMetadata" export default async function RootLayout({ children, @@ -35,8 +30,8 @@ export default async function RootLayout({ >) { setLang(params.lang) preloadUserTracking() - const { defaultLocale, locale, messages } = await getIntl() + return ( diff --git a/app/[lang]/(live)/not-found.tsx b/app/[lang]/(live)/not-found.tsx index 93a118353..b709a17c2 100644 --- a/app/[lang]/(live)/not-found.tsx +++ b/app/[lang]/(live)/not-found.tsx @@ -1,10 +1,6 @@ import { getIntl } from "@/i18n" -import { setLang } from "@/i18n/serverContext" -import { LangParams, PageArgs } from "@/types/params" - -export default async function NotFound({ params }: PageArgs) { - setLang(params.lang) +export default async function NotFound() { const { formatMessage } = await getIntl() return (
diff --git a/app/[lang]/webview/[contentType]/[uid]/page.tsx b/app/[lang]/webview/[contentType]/[uid]/page.tsx index 0efdea462..88bfd57e3 100644 --- a/app/[lang]/webview/[contentType]/[uid]/page.tsx +++ b/app/[lang]/webview/[contentType]/[uid]/page.tsx @@ -21,6 +21,7 @@ export default async function ContentTypePage({ const user = await serverClient().user.get() if (!user) { + console.log(`[webview:page] unable to load user`) return

Error: No user could be loaded

} @@ -31,9 +32,16 @@ export default async function ContentTypePage({ case "token_expired": const h = headers() const returnURL = `/${getLang()}/webview${h.get("x-pathname")!}` - redirect( - `/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` - ) + const redirectURL = `/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` + console.log(`[webview:page] user error, redirecting to: ${redirectURL}`) + redirect(redirectURL) + case "notfound": + return

Error: user not found

+ case "unknown": + return

Unknown error occurred loading user

+ default: + const u: never = user + console.log(`[webview:page] unhandled user loading error`) } } diff --git a/app/api/web/add-card-callback/[lang]/route.ts b/app/api/web/add-card-callback/[lang]/route.ts index 17527a66e..63eb19942 100644 --- a/app/api/web/add-card-callback/[lang]/route.ts +++ b/app/api/web/add-card-callback/[lang]/route.ts @@ -2,46 +2,53 @@ import { NextRequest } from "next/server" import { env } from "process" import { Lang } from "@/constants/languages" +import { profile } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" -import { badRequest, internalServerError } from "@/server/errors/next" export async function GET( request: NextRequest, { params }: { params: { lang: string } } ) { - try { - const lang = params.lang as Lang + console.log(`[add-card] callback started`) + const lang = params.lang as Lang + const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`) + try { const searchParams = request.nextUrl.searchParams const success = searchParams.get("success") const failure = searchParams.get("failure") + const cancel = searchParams.get("cancel") const trxId = searchParams.get("datatransTrxId") - const returnUrl = new URL( - `${env.PUBLIC_URL}/${lang ?? Lang.en}/scandic-friends/my-pages/profile` - ) - if (success) { - if (!trxId) { - return badRequest("Missing datatransTrxId param") - } + if (trxId) { + const saveCardSuccess = await serverClient().user.creditCard.save({ + transactionId: trxId, + }) - const saveCardSuccess = await serverClient().user.saveCard({ - transactionId: trxId, - }) - - if (saveCardSuccess) { - returnUrl.searchParams.set("success", "true") + if (saveCardSuccess) { + console.log(`[add-card] planet success: card saved success`) + returnUrl.searchParams.set("success", "true") + } else { + console.log(`[add-card] planet success: card saved fail`) + returnUrl.searchParams.set("failure", "true") + } } else { - returnUrl.searchParams.set("failure", "true") + console.log(`[add-card] planet success: missing datatransTrxId`) + returnUrl.searchParams.set("error", "true") } } else if (failure) { + console.log(`[add-card] planet fail`) returnUrl.searchParams.set("failure", "true") + } else if (cancel) { + console.log(`[add-card] planet cancel`) + returnUrl.searchParams.set("cancel", "true") } - - return Response.redirect(returnUrl, 307) - } catch (error) { - console.error(error) - return internalServerError() + } catch (e) { + console.error(`[add-card] error saving credit card`, e) + returnUrl.searchParams.set("error", "true") } + + console.log(`[add-card] redirecting to: ${returnUrl}`) + return Response.redirect(returnUrl) } diff --git a/app/globals.css b/app/globals.css index 2bdc25662..b33c7954c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -99,6 +99,7 @@ :root { --max-width: 113.5rem; --max-width-content: 74.75rem; + --max-width-text-block: 49.5rem; } * { diff --git a/auth.ts b/auth.ts index 5dbf1097e..29d470f4d 100644 --- a/auth.ts +++ b/auth.ts @@ -136,7 +136,9 @@ export const config = { return session }, async redirect({ baseUrl, url }) { + console.log(`[auth] deciding redirect URL`, { baseUrl, url }) if (url.startsWith("/")) { + console.log(`[auth] relative URL accepted, returning: ${baseUrl}${url}`) // Allows relative callback URLs return `${baseUrl}${url}` } else { @@ -146,17 +148,19 @@ export const config = { if ( /\.scandichotels\.(dk|de|com|fi|no|se)$/.test(parsedUrl.hostname) ) { + console.log(`[auth] subdomain URL accepted, returning: ${url}`) // Allows any subdomains on all top level domains above return url } else if (parsedUrl.origin === baseUrl) { // Allows callback URLs on the same origin + console.log(`[auth] origin URL accepted, returning: ${url}`) return url } } catch (e) { - console.error("Error in auth redirect callback") - console.error(e) + console.error(`[auth] error parsing incoming URL for redirection`, e) } } + console.log(`[auth] URL denied, returning base URL: ${baseUrl}`) return baseUrl }, async authorized({ auth, request }) { diff --git a/components/ContentType/ContentPage.tsx b/components/ContentType/ContentPage.tsx deleted file mode 100644 index bf2443621..000000000 --- a/components/ContentType/ContentPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default async function ContentPage() { - return null -} diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css new file mode 100644 index 000000000..979fad6e5 --- /dev/null +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -0,0 +1,19 @@ +.contentPage { + padding-bottom: var(--Spacing-x9); +} + +.header { + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x4) var(--Spacing-x2); +} + +.content { + padding: var(--Spacing-x4) var(--Spacing-x2); + display: grid; + justify-items: center; +} + +.innerContent { + width: 100%; + max-width: var(--max-width-content); +} diff --git a/components/ContentType/ContentPage/index.tsx b/components/ContentType/ContentPage/index.tsx new file mode 100644 index 000000000..5f9f27dc9 --- /dev/null +++ b/components/ContentType/ContentPage/index.tsx @@ -0,0 +1,46 @@ +import { serverClient } from "@/lib/trpc/server" + +import Hero from "@/components/Hero" +import Intro from "@/components/Intro" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" +import Title from "@/components/TempDesignSystem/Text/Title" +import TrackingSDK from "@/components/TrackingSDK" + +import styles from "./contentPage.module.css" + +export default async function ContentPage() { + const contentPageRes = await serverClient().contentstack.contentPage.get() + + if (!contentPageRes) { + return null + } + + const { tracking, contentPage } = contentPageRes + const heroImage = contentPage.heroImage + + return ( + <> +
+
+ + {contentPage.header.heading} + {contentPage.header.preamble} + +
+ +
+
+ {heroImage ? ( + + ) : null} +
+
+
+ + + + ) +} diff --git a/components/ContentType/HotelPage/HotelPage.tsx b/components/ContentType/HotelPage/index.tsx similarity index 100% rename from components/ContentType/HotelPage/HotelPage.tsx rename to components/ContentType/HotelPage/index.tsx diff --git a/components/ContentType/LoyaltyPage/LoyaltyPage.tsx b/components/ContentType/LoyaltyPage/index.tsx similarity index 56% rename from components/ContentType/LoyaltyPage/LoyaltyPage.tsx rename to components/ContentType/LoyaltyPage/index.tsx index afa0e405e..d28a3640c 100644 --- a/components/ContentType/LoyaltyPage/LoyaltyPage.tsx +++ b/components/ContentType/LoyaltyPage/index.tsx @@ -1,8 +1,11 @@ import { serverClient } from "@/lib/trpc/server" +import Hero from "@/components/Hero" +import Intro from "@/components/Intro" import { Blocks } from "@/components/Loyalty/Blocks" import Sidebar from "@/components/Loyalty/Sidebar" import MaxWidth from "@/components/MaxWidth" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import TrackingSDK from "@/components/TrackingSDK" @@ -16,7 +19,7 @@ export default async function LoyaltyPage() { } const { tracking, loyaltyPage } = loyaltyPageRes - + const heroImage = loyaltyPage.heroImage return ( <>
@@ -24,8 +27,22 @@ export default async function LoyaltyPage() { ) : null} - - {loyaltyPage.heading} + +
+ + {loyaltyPage.heading} + {loyaltyPage.preamble ? ( + {loyaltyPage.preamble} + ) : null} + + + {heroImage ? ( + + ) : null} +
{loyaltyPage.blocks ? : null}
diff --git a/components/ContentType/LoyaltyPage/loyaltyPage.module.css b/components/ContentType/LoyaltyPage/loyaltyPage.module.css index a0f140faf..45470127c 100644 --- a/components/ContentType/LoyaltyPage/loyaltyPage.module.css +++ b/components/ContentType/LoyaltyPage/loyaltyPage.module.css @@ -15,6 +15,11 @@ padding-right: var(--Spacing-x2); } +.header { + display: grid; + gap: var(--Spacing-x4); +} + @media screen and (min-width: 1367px) { .content { gap: var(--Spacing-x5); diff --git a/components/Current/Header/LoginButton.tsx b/components/Current/Header/LoginButton.tsx index 9e8144641..8634b71bc 100644 --- a/components/Current/Header/LoginButton.tsx +++ b/components/Current/Header/LoginButton.tsx @@ -44,6 +44,7 @@ export default function LoginButton({ id={trackingId} color={color} href={`${login[lang]}?redirectTo=${encodeURIComponent(pathName)}`} + prefetch={false} > {children} diff --git a/components/Current/Header/index.tsx b/components/Current/Header/index.tsx index e590e3f65..0f932cfc8 100644 --- a/components/Current/Header/index.tsx +++ b/components/Current/Header/index.tsx @@ -28,7 +28,7 @@ export default async function Header({ /** * ToDo: Create logic to get this info from ContentStack based on page * */ - const hideBookingWidget = false + const hideBookingWidget = true if (!data) { return null diff --git a/components/Hero/hero.module.css b/components/Hero/hero.module.css new file mode 100644 index 000000000..4ba53fa9f --- /dev/null +++ b/components/Hero/hero.module.css @@ -0,0 +1,14 @@ +.hero { + height: 400px; + margin-bottom: var(--Spacing-x2); + width: 100%; + object-fit: cover; + border-radius: var(--Corner-radius-xLarge); + margin: 0; +} + +@media (min-width: 768px) { + .hero { + height: 480px; + } +} diff --git a/components/Hero/hero.ts b/components/Hero/hero.ts new file mode 100644 index 000000000..29fd1b8d1 --- /dev/null +++ b/components/Hero/hero.ts @@ -0,0 +1,4 @@ +export interface HeroProps { + alt: string + src: string +} diff --git a/components/Hero/index.tsx b/components/Hero/index.tsx new file mode 100644 index 000000000..fade3e6b0 --- /dev/null +++ b/components/Hero/index.tsx @@ -0,0 +1,17 @@ +import Image from "@/components/Image" + +import { HeroProps } from "./hero" + +import styles from "./hero.module.css" + +export default async function Hero({ alt, src }: HeroProps) { + return ( + {alt} + ) +} diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx b/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx new file mode 100644 index 000000000..9482f51fe --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/IntroSection/index.tsx @@ -0,0 +1,57 @@ +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./introSection.module.css" + +import { IntroSectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" + +export default async function IntroSection({ email }: IntroSectionProps) { + const intl = await getIntl() + + return ( +
+
+ + {intl.formatMessage({ id: "Thank you" })} + + + {intl.formatMessage({ id: "We look forward to your visit!" })} + +
+ + {intl.formatMessage({ + id: "We have sent a detailed confirmation of your booking to your email: ", + })} + {email} + +
+ + +
+
+ ) +} diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css b/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css new file mode 100644 index 000000000..5b79c3796 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css @@ -0,0 +1,26 @@ +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); + width: 100%; +} + +.buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x2); +} + +.button { + width: 100%; + max-width: 240px; + justify-content: center; +} + +@media screen and (min-width: 1367px) { + .buttons { + flex-direction: row; + justify-content: space-around; + } +} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx new file mode 100644 index 000000000..99ecae8ca --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx @@ -0,0 +1,80 @@ +import { ArrowRightIcon, ScandicLogoIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./staySection.module.css" + +import { StaySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" + +export default async function StaySection({ hotel, stay }: StaySectionProps) { + const intl = await getIntl() + + const nightsText = + stay.nights > 1 + ? intl.formatMessage({ id: "nights" }) + : intl.formatMessage({ id: "night" }) + + return ( + <> +
+ +
+
+ + + {hotel.name} + + + {hotel.address} + {hotel.phone} + +
+ + {`${stay.nights} ${nightsText}`} + + {stay.start} + + {stay.end} + + +
+
+
+
+ + {intl.formatMessage({ id: "Breakfast" })} + + + {`${intl.formatMessage({ id: "Weekdays" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} + {`${intl.formatMessage({ id: "Weekends" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} + +
+
+ {intl.formatMessage({ id: "Check in" })} + + {intl.formatMessage({ id: "From" })} + {hotel.checkIn} + +
+
+ + {intl.formatMessage({ id: "Check out" })} + + + {intl.formatMessage({ id: "At latest" })} + {hotel.checkOut} + +
+
+ + ) +} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css b/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css new file mode 100644 index 000000000..1eae5c732 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css @@ -0,0 +1,78 @@ +.card { + display: flex; + width: 100%; + background-color: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Small); + overflow: hidden; +} + +.image { + height: 100%; + width: 105px; + object-fit: cover; +} + +.info { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); +} + +.hotel, +.stay { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); +} + +.caption { + display: flex; + flex-direction: column; +} + +.dates { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); +} + +.table { + display: flex; + justify-content: space-between; + padding: var(--Spacing-x2); + border-radius: var(--Corner-radius-Small); + background-color: var(--Base-Surface-Primary-dark-Normal); + width: 100%; +} + +.breakfast, +.checkIn, +.checkOut { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); +} + +@media screen and (min-width: 1367px) { + .card { + flex-direction: column; + } + .image { + width: 100%; + max-height: 195px; + } + + .info { + flex-direction: row; + justify-content: space-between; + } + + .hotel, + .stay { + width: 100%; + max-width: 230px; + } +} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx new file mode 100644 index 000000000..509af9c52 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx @@ -0,0 +1,39 @@ +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./summarySection.module.css" + +import { SummarySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" + +export default async function SummarySection({ summary }: SummarySectionProps) { + const intl = await getIntl() + const roomType = `${intl.formatMessage({ id: "Type of room" })}: ${summary.roomType}` + const bedType = `${intl.formatMessage({ id: "Type of bed" })}: ${summary.bedType}` + const breakfast = `${intl.formatMessage({ id: "Breakfast" })}: ${summary.breakfast}` + const flexibility = `${intl.formatMessage({ id: "Flexibility" })}: ${summary.flexibility}` + + return ( +
+ + {intl.formatMessage({ id: "Summary" })} + + + {roomType} + 1648 SEK + + + {bedType} + 0 SEK + + + {breakfast} + 198 SEK + + + {flexibility} + 200 SEK + +
+ ) +} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css b/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css new file mode 100644 index 000000000..b65d92e76 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css @@ -0,0 +1,13 @@ +.section { + width: 100%; +} + +.summary { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.summary span { + padding: var(--Spacing-x2) var(--Spacing-x0); +} diff --git a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts b/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts new file mode 100644 index 000000000..2dbf572e7 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts @@ -0,0 +1,27 @@ +import { BookingConfirmation } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" + +export const tempConfirmationData: BookingConfirmation = { + email: "lisa.andersson@outlook.com", + hotel: { + name: "Helsinki Hub", + address: "Kaisaniemenkatu 7, Helsinki", + location: "Helsinki", + phone: "+358 300 870680", + image: + "https://test3.scandichotels.com/imagevault/publishedmedia/i11isd60bh119s9486b7/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg?w=640", + checkIn: "15.00", + checkOut: "12.00", + breakfast: { start: "06:30", end: "10:00" }, + }, + stay: { + nights: 1, + start: "2024.03.09", + end: "2024.03.10", + }, + summary: { + roomType: "Standard Room", + bedType: "King size", + breakfast: "Yes", + flexibility: "Yes", + }, +} diff --git a/components/Intro/index.tsx b/components/Intro/index.tsx new file mode 100644 index 000000000..6c7a8a30d --- /dev/null +++ b/components/Intro/index.tsx @@ -0,0 +1,11 @@ +import { PropsWithChildren } from "react" + +import styles from "./intro.module.css" + +export default async function Intro({ children }: PropsWithChildren) { + return ( +
+
{children}
+
+ ) +} diff --git a/components/Intro/intro.module.css b/components/Intro/intro.module.css new file mode 100644 index 000000000..b2290fb9f --- /dev/null +++ b/components/Intro/intro.module.css @@ -0,0 +1,16 @@ +.intro { + max-width: var(--max-width-content); + margin: 0 auto; +} + +.content { + display: grid; + max-width: var(--max-width-text-block); + gap: var(--Spacing-x2); +} + +@media (min-width: 768px) { + .content { + gap: var(--Spacing-x3); + } +} diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx index 40122a9a8..7ebf1874e 100644 --- a/components/JsonToHtml/renderOptions.tsx +++ b/components/JsonToHtml/renderOptions.tsx @@ -347,7 +347,9 @@ export const renderOptions: RenderOptions = { const image = insertResponseToImageVaultAsset(attrs) const alt = image.meta.alt ?? image.title - const width = parseInt(attrs.width.replaceAll("px", "")) + const width = attrs.width + ? parseInt(attrs.width.replaceAll("px", "")) + : image.dimensions.width const props = extractPossibleAttributes(attrs) return (
diff --git a/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/DE.json b/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/DE.json index d792360ac..7df23ad08 100644 --- a/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/DE.json +++ b/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/DE.json @@ -3,7 +3,7 @@ { "level": 1, "name": "New Friend", - "requirement": "0p", + "requirement": "0 Punkte", "description": "Dies ist der Beginn von etwas Wunderbarem: Als New Friend können Sie sich auf eine Reise voller herrlicher Scandic-Entdeckungen freuen.", "icon": "/_static/icons/loyaltylevels/new-friend.svg", "benefits": [ @@ -78,7 +78,7 @@ { "level": 2, "name": "Good Friend", - "requirement": "5 000p", + "requirement": "5 000 Punkte", "description": "Sie waren in letzter Zeit viel bei uns! Und ehrlich gesagt haben wir das Gefühl, dass wir auf einer Wellenlänge sind – die vielen angenehmen Aufenthalte und lustigen Überraschungen sprechen für sich.", "icon": "/_static/icons/loyaltylevels/good-friend.svg", "benefits": [ @@ -153,7 +153,7 @@ { "level": 3, "name": "Close Friend", - "requirement": "10 000p", + "requirement": "10 000 Punkte", "description": "Jetzt wird es ernst: Wir lernen uns wirklich besser kennen, was bedeutet, dass Ihre Zeit mit Scandic noch viel persönlicher wird.", "icon": "/_static/icons/loyaltylevels/close-friend.svg", "benefits": [ @@ -229,7 +229,7 @@ { "level": 4, "name": "Dear Friend", - "requirement": "25 000p", + "requirement": "25 000 Punkte", "description": "Ein Hoch auf uns! Unser Verhältnis scheint sich in Richtung Freunde fürs Leben zu entwickeln – was auch bedeutet, dass Sie Zugang zu einer ganzen Menge mehr Scandic bekommen.", "icon": "/_static/icons/loyaltylevels/dear-friend.svg", "benefits": [ @@ -306,7 +306,7 @@ { "level": 5, "name": "Loyal Friend", - "requirement": "100 000p", + "requirement": "100 000 Punkte", "description": "Sie haben uns während zahlreicher Aufenthalte, Happy Hours und Workouts im Fitnessstudio die Treue gehalten – deshalb wollen wir uns mit einigen unserer großartigsten Belohnungen bei Ihnen revanchieren.", "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", "benefits": [ @@ -383,7 +383,7 @@ { "level": 6, "name": "True Friend", - "requirement": "250 000p", + "requirement": "250 000 Punkte", "description": "Es spielt keine Rolle, ob Haupt- oder Nebensaison: Sie sind immer für uns da. Genießen Sie noch mehr individuelle Vorteile – genau nach Ihrem Geschmack.", "icon": "/_static/icons/loyaltylevels/true-friend.svg", "benefits": [ @@ -460,7 +460,7 @@ { "level": 7, "name": "Best Friend", - "requirement": "400 000p oder 100 nächte", + "requirement": "400 000 Punkte oder 100 Nächte", "description": "Für eine Freundschaft wie diese gibt es im Grunde keine passenden Worte, aber wir versuchen es trotzdem: Denn es könnte gar nichts Besseres geben, wenn es um sehr, sehr exklusive Erlebnisse geht!", "icon": "/_static/icons/loyaltylevels/best-friend.svg", "benefits": [ diff --git a/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/FI.json b/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/FI.json index 8a7f6ec6b..11df9e8ff 100644 --- a/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/FI.json +++ b/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/FI.json @@ -3,29 +3,29 @@ { "level": 1, "name": "New Friend", - "requirement": "0p", - "description": "Olemme uuden ja upean kynnyksellä: New Friend -ystävänä pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.", + "requirement": "0 p", + "description": "Ystävänämme pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.", "icon": "/_static/icons/loyaltylevels/new-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", "description": "Mikä herkullinen etu! Hyödynnä 10 %:n alennus hotelliemme ravintoloissa ja shopissa viikonloppuisin. Tarjous on voimassa niin majoittujille kuin hotellitunnelmaa hetkeksi etsiville. Hemmottele siis itseäsi ja löydä tie lähimpään Scandiciin.", "unlocked": true, - "value": "10%" + "value": "10 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { @@ -39,38 +39,38 @@ "unlocked": false }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": false }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": false }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": false }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": false }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": false } ] @@ -78,29 +78,29 @@ { "level": 2, "name": "Good Friend", - "requirement": "5 000p", - "description": "Kiva, että olet vieraillut meillä, ja tuntuu, että ystävyytemme on hyvässä nosteessa. Tästä on hyvä jatkaa, yksi yöpyminen ja iloinen yllätys kerrallaan!", + "requirement": "5 000 p", + "description": "Tästä on hyvä jatkaa, yksi yöpyminen ja iloinen yllätys kerrallaan!", "icon": "/_static/icons/loyaltylevels/good-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", - "description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!", + "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { @@ -114,38 +114,38 @@ "unlocked": false }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": false }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": false }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": false }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": false }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": false } ] @@ -153,29 +153,29 @@ { "level": 3, "name": "Close Friend", - "requirement": "10 000p", - "description": "Onpa kiva, että olet vieraillut meillä näin usein! Nyt etusi vain paranevat, sillä olemmehan jo enemmän kuin hyvän päivän tuttuja.", + "requirement": "10 000 p", + "description": "Nyt etusi vain paranevat, sillä olemmehan jo enemmän kuin hyvän päivän tuttuja.", "icon": "/_static/icons/loyaltylevels/close-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", - "description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!", + "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { @@ -190,38 +190,38 @@ "unlocked": false }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": false }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": false }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": false }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": false }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": false } ] @@ -229,29 +229,29 @@ { "level": 4, "name": "Dear Friend", - "requirement": "25 000p", + "requirement": "25 000 p", "description": "Kippis syventyvälle ystävyydellemme. Nyt pääset nauttimaan liudasta uusia etuja.", "icon": "/_static/icons/loyaltylevels/dear-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", - "description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!", + "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { @@ -264,41 +264,41 @@ "name": "Enemmän pisteitä", "description": "Tässä lisäboostia sinulle: saat 25 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa.", "unlocked": true, - "value": "25%" + "value": "25 %" }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": false }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": false }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": false }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": false }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": false }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": false } ] @@ -306,29 +306,29 @@ { "level": 5, "name": "Loyal Friend", - "requirement": "100 000p", - "description": "Kiva, että olemme saaneet jakaa paljon yhteisiä hetkiä. Olet tosiaan nimesi arvoinen Loyal Friend! Haluamme panostaa ystävyyteemme myös jatkossa ja annammekin sinulle kasan uusia, ihania etuja.", + "requirement": "100 000 p", + "description": "Haluamme panostaa ystävyyteemme myös jatkossa ja annammekin sinulle kasan uusia, ihania etuja.", "icon": "/_static/icons/loyaltylevels/loyal-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", - "description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!", + "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { @@ -341,41 +341,41 @@ "name": "Enemmän pisteitä", "description": "Tässä lisäboostia sinulle: saat 25 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa.", "unlocked": true, - "value": "25%" + "value": "25 %" }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": true }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": false }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": false }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": false }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": false } ] @@ -383,29 +383,29 @@ { "level": 6, "name": "True Friend", - "requirement": "250 000p", - "description": "Onpa ollut ihana nähdä sinua näin paljon viime aikoina. Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.", + "requirement": "250 000 p", + "description": "Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.", "icon": "/_static/icons/loyaltylevels/true-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", - "description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!", + "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { @@ -416,43 +416,43 @@ }, { "name": "Enemmän pisteitä", - "description": "Tässä extra-boostia sinulle: saat 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa. ", + "description": "Saat 25 % tai 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä!.", "unlocked": true, - "value": "50%" + "value": "50 %" }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": true }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": true }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": true }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": false }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": false } ] @@ -460,76 +460,76 @@ { "level": 7, "name": "Best Friend", - "requirement": "400 000p tai 100 yötä", - "description": "Ystävyytemme on vailla vertaa. Koska sanat eivät riitä kiittämään ystävyydestämme, pääset nyt käsiksi kaikkein eksklusiivisimpiin elämyksiin.", + "requirement": "400 000 p tai 100 yötä", + "description": "Koska sanat eivät riitä kiittämään ystävyydestämme, pääset nyt käsiksi kaikkein eksklusiivisimpiin elämyksiin.", "icon": "/_static/icons/loyaltylevels/best-friend.svg", "benefits": [ { "name": "Ystävähinnat", - "description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.", + "description": "Ystävänämme saat aina parhaan hinnan.", "unlocked": true }, { "name": "Alennus ruoasta", - "description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!", + "description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { - "name": "Mocktail lapsille yöpymisen yhteydessä maksutta", - "description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", + "name": "Mocktail lapsille maksutta", + "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.", "unlocked": true }, { - "name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", - "description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.", + "name": "Myöhäinen uloskirjautuminen", + "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { "name": "Ravintolakuponki", - "description": "Parhaana ystävänämme saat 20 € ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä. Illallinen hotellin ravintolassa tai kasa herkkuja huoneeseen – mihin sinä sen käyttäisit?", + "description": "Ystävänämme saat ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä.", "unlocked": true, "value": "20 €" }, { "name": "Enemmän pisteitä", - "description": "Tässä extra-boostia sinulle: saat 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa. ", + "description": "Saat 25 % tai 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä!.", "unlocked": true, - "value": "50%" + "value": "50 %" }, { - "name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", - "description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", + "name": "Aikainen sisäänkirjautuminen", + "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { - "name": "Maksuton huoneluokan korotus varaustilanteen mukaan", - "description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", + "name": "Maksuton huoneluokan korotus", + "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.", "unlocked": true }, { "name": "Aamiainen – kaksi yhden hinnalla", - "description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.", + "description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.", "unlocked": true }, { "name": "48 tunnin huonetakuu", - "description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?", + "description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.", "unlocked": true }, { "name": "Aamiainen aina maksutta", - "description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.", + "description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.", "unlocked": true }, { "name": "Upea vuotuinen lahja", - "description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.", + "description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.", "unlocked": true }, { "name": "Kid’s boost", - "description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!", + "description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.", "unlocked": true } ] diff --git a/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/NO.json b/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/NO.json index 7c8002d24..52cfdc49a 100644 --- a/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/NO.json +++ b/components/Loyalty/Blocks/DynamicContent/OverviewTable/data/NO.json @@ -16,7 +16,7 @@ "name": "Rabatt på mat", "description": "Nam! Nyt en smakfull 10 % rabatt i restauranten og shoppen vår i helgene. Dette tilbudet gjelder enten du er gjesten vår over natten eller bare kommer innom for en matbit. Så, sett i gang, unn deg selv noe godt.", "unlocked": true, - "value": "10%" + "value": "10 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -34,7 +34,7 @@ "unlocked": false }, { - "name": "Ekstra vennskap", + "name": "Friendsboost", "description": "", "unlocked": false }, @@ -89,9 +89,9 @@ }, { "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", + "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -109,7 +109,7 @@ "unlocked": false }, { - "name": "Ekstra vennskap", + "name": "Friendsboost", "description": "", "unlocked": false }, @@ -164,9 +164,9 @@ }, { "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du bor hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", + "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15 % rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du bor hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -185,7 +185,7 @@ "value": "50 NOK" }, { - "name": "Ekstra vennskap", + "name": "Friendsboost", "description": "", "unlocked": false }, @@ -240,9 +240,9 @@ }, { "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", + "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -261,10 +261,10 @@ "value": "75 NOK" }, { - "name": "Ekstra vennskap", + "name": "Friendsboost", "description": "Her har du noe veldig bra: Hver gang du øker antall vennskapspoeng, får du 25 % ekstra – ekstra på det ekstra! Så, begynn å samle poeng på opphold, måltider og mer, og du vil veldig snart få et gratis opphold.", "unlocked": true, - "value": "25%" + "value": "25 %" }, { "name": "Tidlig innsjekk når tilgjengelig", @@ -317,9 +317,9 @@ }, { "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", + "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -338,10 +338,10 @@ "value": "100 NOK" }, { - "name": "Ekstra vennskap", + "name": "Friendsboost", "description": "Her har du noe veldig bra: Hver gang du øker antall vennskapspoeng, får du 25 % ekstra – ekstra på det ekstra! Så, begynn å samle poeng på opphold, måltider og mer, og du vil veldig snart få et gratis opphold.", "unlocked": true, - "value": "25%" + "value": "25 %" }, { "name": "Tidlig innsjekk når tilgjengelig", @@ -394,9 +394,9 @@ }, { "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", + "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -415,10 +415,10 @@ "value": "150 NOK" }, { - "name": "Ekstra vennskap", - "description": "Du kan virkelig glede deg. Hver gang du øker antall vennskapspoeng, får du 50 % ekstra – ekstra på det ekstra! Så, få flere poeng på opphold, måltider og mer, og du vil få et gratis opphold lynraskt", + "name": "Friendsboost", + "description": "Gled deg! Hver gang du tjener nye Friends-poeng får du 25 % eller 50 % ekstra poeng – som en superboost! Begynn å tjene poeng ved å bo og spise hos oss, og du vil få en bonusnatt før du aner det.", "unlocked": true, - "value": "50%" + "value": "50 %" }, { "name": "Tidlig innsjekk når tilgjengelig", @@ -471,9 +471,9 @@ }, { "name": "Rabatt på mat", - "description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.", + "description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.", "unlocked": true, - "value": "15%" + "value": "15 %" }, { "name": "Gratis barne-mocktail under oppholdet", @@ -492,10 +492,10 @@ "value": "200 NOK" }, { - "name": "Ekstra vennskap", - "description": "Du kan virkelig glede deg. Hver gang du øker antall vennskapspoeng, får du 50 % ekstra – ekstra på det ekstra! Så, få flere poeng på opphold, måltider og mer, og du vil få et gratis opphold lynraskt", + "name": "Friendsboost", + "description": "Gled deg! Hver gang du tjener nye Friends-poeng får du 25 % eller 50 % ekstra poeng – som en superboost! Begynn å tjene poeng ved å bo og spise hos oss, og du vil få en bonusnatt før du aner det.", "unlocked": true, - "value": "50%" + "value": "50 %" }, { "name": "Tidlig innsjekk når tilgjengelig", diff --git a/components/Loyalty/Sidebar/MyPagesNavigation/index.tsx b/components/Loyalty/Sidebar/MyPagesNavigation/index.tsx new file mode 100644 index 000000000..19ea70405 --- /dev/null +++ b/components/Loyalty/Sidebar/MyPagesNavigation/index.tsx @@ -0,0 +1,13 @@ +import { serverClient } from "@/lib/trpc/server" + +import MyPagesSidebar from "@/components/MyPages/Sidebar" + +export async function MyPagesNavigation() { + const user = await serverClient().user.name() + + // Check if we have user, that means we are logged in andt the My Pages menu can show. + if (!user) { + return null + } + return +} diff --git a/components/Loyalty/Sidebar/index.tsx b/components/Loyalty/Sidebar/index.tsx index 912d86e27..53f0137b8 100644 --- a/components/Loyalty/Sidebar/index.tsx +++ b/components/Loyalty/Sidebar/index.tsx @@ -1,7 +1,7 @@ import JsonToHtml from "@/components/JsonToHtml" -import SidebarMyPages from "@/components/MyPages/Sidebar" import JoinLoyaltyContact from "./JoinLoyalty" +import { MyPagesNavigation } from "./MyPagesNavigation" import styles from "./sidebar.module.css" @@ -38,7 +38,7 @@ export default function SidebarLoyalty({ blocks }: SidebarProps) { case SidebarTypenameEnum.LoyaltyPageSidebarDynamicContent: switch (block.dynamic_content.component) { case LoyaltySidebarDynamicComponentEnum.my_pages_navigation: - return + return default: return null } diff --git a/components/MyPages/Blocks/Overview/Friend/index.tsx b/components/MyPages/Blocks/Overview/Friend/index.tsx index b1c41884a..c1b89c3e6 100644 --- a/components/MyPages/Blocks/Overview/Friend/index.tsx +++ b/components/MyPages/Blocks/Overview/Friend/index.tsx @@ -31,7 +31,7 @@ export default async function Friend({ {formatMessage( isHighestLevel ? { id: "Highest level" } - : { id: "Your current level" } + : { id: `Level ${membershipLevels[membership.membershipLevel]}` } )} {membership ? ( diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx index f0cc9b2ef..3e08de171 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx @@ -7,9 +7,8 @@ import { trpc } from "@/lib/trpc/client" import LoadingSpinner from "@/components/LoadingSpinner" -import DesktopTable from "./Desktop" -import MobileTable from "./Mobile" import Pagination from "./Pagination" +import Table from "./Table" import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn" @@ -40,8 +39,7 @@ export default function TransactionTable({ ) : ( <> - - + {data && data.meta.totalPages > 1 ? ( 0) { - variant = "addition" - } else if (awardPoints < 0) { - variant = "negation" - awardPoints = Math.abs(awardPoints) - } - - const classNames = awardPointsVariants({ - variant, - }) - - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) - return -} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/index.tsx deleted file mode 100644 index 297aa39cf..000000000 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" - -import useLang from "@/hooks/useLang" - -import AwardPoints from "./AwardPoints" - -import styles from "./row.module.css" - -import type { RowProps } from "@/types/components/myPages/myPage/earnAndBurn" - -export default function Row({ transaction }: RowProps) { - const intl = useIntl() - const lang = useLang() - const description = - transaction.hotelName && transaction.city - ? `${transaction.hotelName}, ${transaction.city} ${transaction.nights} ${intl.formatMessage({ id: "nights" })}` - : `${transaction.nights} ${intl.formatMessage({ id: "nights" })}` - const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY") - const departure = dt(transaction.checkoutDate) - .locale(lang) - .format("DD MMM YYYY") - return ( - - - - - - - - ) -} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Mobile/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Mobile/index.tsx deleted file mode 100644 index d4de69ac4..000000000 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Mobile/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useIntl } from "react-intl" - -import { dt } from "@/lib/dt" - -import AwardPoints from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/AwardPoints" -import Body from "@/components/TempDesignSystem/Text/Body" -import useLang from "@/hooks/useLang" - -import styles from "./mobile.module.css" - -import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn" - -export default function MobileTable({ transactions }: TableProps) { - const intl = useIntl() - const lang = useLang() - return ( -
-
{formatter.format(awardPoints)} pts
{arrival}{description}{transaction.confirmationNumber}{departure}
- - - - - - - - - - - - {transactions.length ? ( - transactions.map((transaction, idx) => ( - - - - - )) - ) : ( - - - - )} - -
- {intl.formatMessage({ id: "Transactions" })} - - {intl.formatMessage({ id: "Points" })} -
- - {dt(transaction.checkinDate) - .locale(lang) - .format("DD MMM YYYY")} - - {transaction.hotelName && transaction.city ? ( - {`${transaction.hotelName}, ${transaction.city}`} - ) : null} - - {`${transaction.nights} ${intl.formatMessage({ id: transaction.nights === 1 ? "night" : "nights" })}`} - -
- {intl.formatMessage({ - id: "There are no transactions to display", - })} -
- - ) -} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Mobile/mobile.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Mobile/mobile.module.css deleted file mode 100644 index ac7e30bda..000000000 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Mobile/mobile.module.css +++ /dev/null @@ -1,52 +0,0 @@ -.table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; -} - -.thead { - background-color: var(--Main-Grey-10); -} - -.th { - padding: var(--Spacing-x2); -} - -.tr { - border-top: 1px solid var(--Main-Grey-10); -} - -.td { - padding: var(--Spacing-x2); -} - -.transactionDetails { - display: grid; - font-size: var(--typography-Footnote-Regular-fontSize); -} - -.transactionDate { - font-weight: 700; -} - -.placeholder { - text-align: center; - padding: var(--Spacing-x4); - border: 1px solid var(--Main-Grey-10); -} -.loadMoreButton { - background-color: var(--Main-Grey-10); - border: none; - display: flex; - align-items: center; - justify-content: center; - gap: var(--Spacing-x-half); - padding: var(--Spacing-x2); - width: 100%; -} - -@media screen and (min-width: 768px) { - .container { - display: none; - } -} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/AwardPoints.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/AwardPoints.tsx new file mode 100644 index 000000000..a2d281dca --- /dev/null +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/AwardPoints.tsx @@ -0,0 +1,40 @@ +import { useIntl } from "react-intl" + +import { Lang } from "@/constants/languages" + +import { awardPointsVariants } from "./awardPointsVariants" + +import type { AwardPointsVariantProps } from "@/types/components/myPages/myPage/earnAndBurn" + +export default function AwardPoints({ + awardPoints, + isCalculated, +}: { + awardPoints: number + isCalculated: boolean +}) { + let variant: AwardPointsVariantProps["variant"] = undefined + const intl = useIntl() + + if (isCalculated) { + if (awardPoints > 0) { + variant = "addition" + } else if (awardPoints < 0) { + variant = "negation" + awardPoints = Math.abs(awardPoints) + } + } + const classNames = awardPointsVariants({ + variant, + }) + + // sv hardcoded to force space on thousands + const formatter = new Intl.NumberFormat(Lang.sv) + return ( + + {isCalculated + ? formatter.format(awardPoints) + : intl.formatMessage({ id: "Points being calculated" })} + + ) +} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/awardPointsVariants.ts b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants.ts similarity index 100% rename from components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/awardPointsVariants.ts rename to components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants.ts diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/index.tsx new file mode 100644 index 000000000..c3fd5b5a5 --- /dev/null +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/index.tsx @@ -0,0 +1,82 @@ +"use client" + +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import Link from "@/components/TempDesignSystem/Link" +import useLang from "@/hooks/useLang" + +import AwardPoints from "./AwardPoints" + +import styles from "./row.module.css" + +import type { RowProps } from "@/types/components/myPages/myPage/earnAndBurn" +import { RewardTransactionTypes } from "@/types/components/myPages/myPage/enums" + +export default function Row({ transaction }: RowProps) { + const intl = useIntl() + const lang = useLang() + + const nightString = `${transaction.nights} ${transaction.nights === 1 ? intl.formatMessage({ id: "night" }) : intl.formatMessage({ id: "nights" })}` + + let description = + transaction.hotelName && transaction.city + ? `${transaction.hotelName}, ${transaction.city} ${nightString}` + : `${nightString}` + + switch (transaction.type) { + case RewardTransactionTypes.stay: + if (transaction.hotelId === "ORS") + description = intl.formatMessage({ id: "Former Scandic Hotel" }) + break + case RewardTransactionTypes.ancillary: + description = intl.formatMessage({ id: "Extras to your booking" }) + break + case RewardTransactionTypes.enrollment: + description = intl.formatMessage({ id: "Sign up bonus" }) + break + case RewardTransactionTypes.mastercard_points: + description = intl.formatMessage({ id: "Scandic Friends Mastercard" }) + break + case RewardTransactionTypes.tui_points: + description = intl.formatMessage({ id: "TUI Points" }) + case RewardTransactionTypes.stayAdj: + if (transaction.confirmationNumber === "BALFWD") + description = intl.formatMessage({ + id: "Points earned prior to May 1, 2021", + }) + break + case RewardTransactionTypes.pointShop: + description = intl.formatMessage({ id: "Scandic Friends Point Shop" }) + break + } + + const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY") + const transactionDate = dt(transaction.transactionDate) + .locale(lang) + .format("DD MMM YYYY") + + return ( + + + {description} + + {transaction.type === RewardTransactionTypes.stay && + transaction.bookingUrl ? ( + + {transaction.confirmationNumber} + + ) : ( + transaction.confirmationNumber + )} + + + {transaction.checkinDate ? arrival : transactionDate} + + + ) +} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/row.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css similarity index 50% rename from components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/row.module.css rename to components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css index 1a75072da..eb59b55ea 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/row.module.css +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css @@ -1,13 +1,21 @@ .tr { - border: 1px solid #e6e9ec; + border-bottom: 1px solid var(--Scandic-Brand-Pale-Peach); + &:last-child { + border-bottom: none; + } } .td { background-color: #fff; color: var(--UI-Text-High-contrast); - padding: var(--Spacing-x2) var(--Spacing-x4); + padding: var(--Spacing-x2); position: relative; text-align: left; + text-wrap: nowrap; +} + +.description { + font-weight: var(--typography-Body-Bold-fontWeight); } .addition { @@ -17,8 +25,7 @@ .addition::before { color: var(--Secondary-Light-On-Surface-Accent); content: "+"; - left: var(--Spacing-x2); - position: absolute; + margin-right: var(--Spacing-x-half); } .negation { @@ -28,6 +35,11 @@ .negation::before { color: var(--Base-Text-Accent); content: "-"; - left: var(--Spacing-x2); - position: absolute; + margin-right: var(--Spacing-x-half); +} + +@media screen and (min-width: 768px) { + .td { + padding: var(--Spacing-x3); + } } diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx similarity index 54% rename from components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/index.tsx rename to components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx index 3d50eb609..c49695e88 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/index.tsx +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx @@ -1,50 +1,48 @@ +"use client" + import { useIntl } from "react-intl" import Body from "@/components/TempDesignSystem/Text/Body" import Row from "./Row" -import styles from "./desktop.module.css" +import styles from "./table.module.css" import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn" const tableHeadings = [ - "Arrival date", + "Points", "Description", "Booking number", - "Transaction date", - "Points", + "Arrival date", ] -export default function DesktopTable({ transactions }: TableProps) { +export default function Table({ transactions }: TableProps) { const intl = useIntl() - return (
{transactions.length ? ( -
- - - - {tableHeadings.map((heading) => ( - - ))} - - - - {transactions.map((transaction, idx) => ( - +
- - {intl.formatMessage({ id: heading })} - -
+ + + {tableHeadings.map((heading) => ( + ))} - -
+ + {intl.formatMessage({ id: heading })} + +
-
+ + + + {transactions.map((transaction, index) => ( + + ))} + + ) : ( diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/desktop.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css similarity index 79% rename from components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/desktop.module.css rename to components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css index 113c23aba..8c319b619 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/desktop.module.css +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css @@ -1,5 +1,8 @@ .container { - display: none; + display: flex; + flex-direction: column; + overflow-x: auto; + border-radius: var(--Corner-radius-Small); } .table { @@ -17,7 +20,8 @@ .th { text-align: left; - padding: 20px 32px; + text-wrap: nowrap; + padding: var(--Spacing-x2); } .placeholder { @@ -49,9 +53,10 @@ } @media screen and (min-width: 768px) { .container { - display: flex; - flex-direction: column; - gap: 16px; - overflow-x: auto; + border-radius: var(--Corner-radius-Large); + } + + .th { + padding: var(--Spacing-x2) var(--Spacing-x3); } } diff --git a/components/MyPages/Breadcrumbs/index.tsx b/components/MyPages/Breadcrumbs/index.tsx index 3fccd6f25..752f77d5f 100644 --- a/components/MyPages/Breadcrumbs/index.tsx +++ b/components/MyPages/Breadcrumbs/index.tsx @@ -11,6 +11,7 @@ export default async function Breadcrumbs() { if (!breadcrumbs?.length) { return null } + const homeBreadcrumb = breadcrumbs.shift() return ( + + + ) +} diff --git a/components/Profile/DeleteCreditCardButton/index.tsx b/components/Profile/DeleteCreditCardButton/index.tsx new file mode 100644 index 000000000..16448d0b1 --- /dev/null +++ b/components/Profile/DeleteCreditCardButton/index.tsx @@ -0,0 +1,40 @@ +"use client" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" + +import { Delete } from "@/components/Icons" +import LoadingSpinner from "@/components/LoadingSpinner" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" + +export default function DeleteCreditCardButton({ + creditCardId, +}: { + creditCardId: string +}) { + const { formatMessage } = useIntl() + const trpcUtils = trpc.useUtils() + + const deleteCreditCardMutation = trpc.user.creditCard.delete.useMutation({ + onSuccess() { + trpcUtils.user.creditCards.invalidate() + toast.success(formatMessage({ id: "Credit card deleted successfully" })) + }, + onError() { + toast.error( + formatMessage({ + id: "Failed to delete credit card, please try again later.", + }) + ) + }, + }) + async function handleDelete() { + deleteCreditCardMutation.mutate({ creditCardId }) + } + return ( + + ) +} diff --git a/components/Profile/DeleteCreditCardConfirmation/deleteCreditCardConfirmation.module.css b/components/Profile/DeleteCreditCardConfirmation/deleteCreditCardConfirmation.module.css new file mode 100644 index 000000000..ed6c3170d --- /dev/null +++ b/components/Profile/DeleteCreditCardConfirmation/deleteCreditCardConfirmation.module.css @@ -0,0 +1,70 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: var(--visual-viewport-height); + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.modal section { + background: var(--Main-Grey-White); + border-radius: var(--Corner-radius-Medium); + padding: var(--Spacing-x4); + padding-bottom: var(--Spacing-x6); +} + +.container { + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); + font-family: var(--typography-Body-Regular-fontFamily); +} + +.title { + font-family: var(--typography-Subtitle-1-fontFamily); + text-align: center; + margin: 0; + padding-bottom: var(--Spacing-x1); +} + +.bodyText { + text-align: center; + max-width: 425px; + margin: 0; + padding: 0; +} + +.buttonContainer { + display: flex; + justify-content: space-between; + gap: var(--Spacing-x2); + flex-wrap: wrap; +} + +.buttonContainer button { + flex-grow: 1; + justify-content: center; +} + +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} diff --git a/components/Profile/DeleteCreditCardConfirmation/index.tsx b/components/Profile/DeleteCreditCardConfirmation/index.tsx new file mode 100644 index 000000000..7d7110c1a --- /dev/null +++ b/components/Profile/DeleteCreditCardConfirmation/index.tsx @@ -0,0 +1,99 @@ +"use client" + +import { + Dialog, + DialogTrigger, + Heading, + Modal, + ModalOverlay, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { trpc } from "@/lib/trpc/client" + +import { Delete } from "@/components/Icons" +import LoadingSpinner from "@/components/LoadingSpinner" +import Button from "@/components/TempDesignSystem/Button" +import { toast } from "@/components/TempDesignSystem/Toasts" + +import styles from "./deleteCreditCardConfirmation.module.css" + +import type { DeleteCreditCardConfirmationProps } from "@/types/components/myPages/myProfile/creditCards" + +export default function DeleteCreditCardConfirmation({ + card, +}: DeleteCreditCardConfirmationProps) { + const intl = useIntl() + const trpcUtils = trpc.useUtils() + + const deleteCard = trpc.user.creditCard.delete.useMutation({ + onSuccess() { + trpcUtils.user.creditCards.invalidate() + + toast.success( + intl.formatMessage({ id: "Your card was successfully removed!" }) + ) + }, + onError() { + toast.error( + intl.formatMessage({ + id: "Something went wrong and we couldn't remove your card. Please try again later.", + }) + ) + }, + }) + + const lastFourDigits = card.truncatedNumber.slice(-4) + + return ( +
+ + + + + + {({ close }) => ( +
+ + {intl.formatMessage({ + id: "Remove card from member profile", + })} + +

+ {`${intl.formatMessage({ + id: "Are you sure you want to remove the card ending with", + })} ${lastFourDigits} ${intl.formatMessage({ id: "from your member profile?" })}`} +

+ + {deleteCard.isPending ? ( + + ) : ( +
+ + +
+ )} +
+ )} +
+
+
+
+
+ ) +} diff --git a/components/TempDesignSystem/Button/button.ts b/components/TempDesignSystem/Button/button.ts index cf391bc56..618ff8caa 100644 --- a/components/TempDesignSystem/Button/button.ts +++ b/components/TempDesignSystem/Button/button.ts @@ -1,9 +1,20 @@ import { buttonVariants } from "./variants" import type { VariantProps } from "class-variance-authority" +import type { ButtonProps as ReactAriaButtonProps } from "react-aria-components" -export interface ButtonProps +export interface ButtonPropsRAC + extends Omit, + VariantProps { + asChild?: false | undefined | never + disabled?: ReactAriaButtonProps["isDisabled"] + onClick?: ReactAriaButtonProps["onPress"] +} + +export interface ButtonPropsSlot extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild: true } + +export type ButtonProps = ButtonPropsSlot | ButtonPropsRAC diff --git a/components/TempDesignSystem/Button/index.tsx b/components/TempDesignSystem/Button/index.tsx index 5acb884b1..38261e61a 100644 --- a/components/TempDesignSystem/Button/index.tsx +++ b/components/TempDesignSystem/Button/index.tsx @@ -1,23 +1,16 @@ "use client" import { Slot } from "@radix-ui/react-slot" +import { Button as ButtonRAC } from "react-aria-components" import { buttonVariants } from "./variants" import type { ButtonProps } from "./button" -export default function Button({ - asChild = false, - theme, - className, - disabled, - intent, - size, - variant, - wrapping, - ...props -}: ButtonProps) { - const Comp = asChild ? Slot : "button" +export default function Button(props: ButtonProps) { + const { className, intent, size, theme, wrapping, variant, ...restProps } = + props + const classNames = buttonVariants({ className, intent, @@ -26,5 +19,19 @@ export default function Button({ wrapping, variant, }) - return + + if (restProps.asChild) { + const { asChild, ...slotProps } = restProps + return + } + + const { asChild, onClick, disabled, ...racProps } = restProps + return ( + + ) } diff --git a/components/TempDesignSystem/LoyaltyCard/loyaltyCard.ts b/components/TempDesignSystem/LoyaltyCard/loyaltyCard.ts index 784d643ad..d5941ef5e 100644 --- a/components/TempDesignSystem/LoyaltyCard/loyaltyCard.ts +++ b/components/TempDesignSystem/LoyaltyCard/loyaltyCard.ts @@ -2,7 +2,7 @@ import { loyaltyCardVariants } from "./variants" import type { VariantProps } from "class-variance-authority" -import { ImageVaultAsset } from "@/types/components/imageVaultImage" +import { ImageVaultAsset } from "@/types/components/imageVault" export interface LoyaltyCardProps extends React.HTMLAttributes, diff --git a/components/TempDesignSystem/Toasts/index.tsx b/components/TempDesignSystem/Toasts/index.tsx index 5e3ad1922..486bc4f46 100644 --- a/components/TempDesignSystem/Toasts/index.tsx +++ b/components/TempDesignSystem/Toasts/index.tsx @@ -16,7 +16,7 @@ import { toastVariants } from "./variants" import styles from "./toasts.module.css" export function ToastHandler() { - return + return } function getIcon(variant: ToastsProps["variant"]) { diff --git a/components/TempDesignSystem/Toasts/toasts.module.css b/components/TempDesignSystem/Toasts/toasts.module.css index cc7b305fb..27ebcff09 100644 --- a/components/TempDesignSystem/Toasts/toasts.module.css +++ b/components/TempDesignSystem/Toasts/toasts.module.css @@ -31,8 +31,9 @@ .iconContainer { display: flex; - background-color: var(--icon-background-color); - padding: var(--Spacing-x2); align-items: center; justify-content: center; + background-color: var(--icon-background-color); + padding: var(--Spacing-x2); + height: 100%; } diff --git a/constants/routes/handleAuth.js b/constants/routes/handleAuth.js index 46d890133..0879d580f 100644 --- a/constants/routes/handleAuth.js +++ b/constants/routes/handleAuth.js @@ -12,6 +12,16 @@ export const login = { sv: "/sv/logga-in", } +/** @type {import('@/types/routes').LangRoute} */ +export const loginUnLocalized = { + da: "/da/login", + de: "/de/login", + en: "/en/login", + fi: "/fi/login", + no: "/no/login", + sv: "/sv/login", +} + /** @type {import('@/types/routes').LangRoute} */ export const logout = { da: "/da/log-ud", @@ -22,6 +32,16 @@ export const logout = { sv: "/sv/logga-ut", } +/** @type {import('@/types/routes').LangRoute} */ +export const logoutUnLocalized = { + da: "/da/logout", + de: "/de/logout", + en: "/en/logout", + fi: "/fi/logout", + no: "/no/logout", + sv: "/sv/logout", +} + /** @type {import('@/types/routes').LangRoute} */ export const verifymagiclink = { da: "/da/verifymagiclink", @@ -36,4 +56,6 @@ export const handleAuth = [ ...Object.values(login), ...Object.values(logout), ...Object.values(verifymagiclink), + ...Object.values(loginUnLocalized), + ...Object.values(logoutUnLocalized), ] diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 4af82935b..3531fa46c 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter", "Already a friend?": "Allerede en ven?", "Amenities": "Faciliteter", + "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.", + "Are you sure you want to remove the card ending with": "Er du sikker på, at du vil fjerne kortet, der slutter med", "Arrival date": "Ankomstdato", "as of today": "fra idag", "As our": "Som vores", @@ -31,6 +33,7 @@ "Could not find requested resource": "Kunne ikke finde den anmodede ressource", "Country": "Land", "Country code": "Landekode", + "Credit card deleted successfully": "Kreditkort blev slettet", "Your current level": "Dit nuværende niveau", "Current password": "Nuværende kodeord", "characters": "tegn", @@ -42,13 +45,24 @@ "Edit": "Redigere", "Edit profile": "Rediger profil", "Email": "E-mail", + "Extras to your booking": "Ekstra til din booking", "There are no transactions to display": "Der er ingen transaktioner at vise", "Explore all levels and benefits": "Udforsk alle niveauer og fordele", + "Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.", "Find booking": "Find booking", "Flexibility": "Fleksibilitet", + "Former Scandic Hotel": "Tidligere Scandic Hotel", "From": "Fra", + "from your member profile?": "fra din medlemsprofil?", "Get inspired": "Bliv inspireret", "Go back to overview": "Gå tilbage til oversigten", + "Level 1": "Niveau 1", + "Level 2": "Niveau 2", + "Level 3": "Niveau 3", + "Level 4": "Niveau 4", + "Level 5": "Niveau 5", + "Level 6": "Niveau 6", + "Level 7": "Niveau 7", "Highest level": "Højeste niveau", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", @@ -73,6 +87,7 @@ "Next": "Næste", "next level:": "Næste niveau:", "No content published": "Intet indhold offentliggjort", + "No, keep card": "Nej, behold kortet", "No transactions available": "Ingen tilgængelige transaktioner", "Not found": "Ikke fundet", "night": "nat", @@ -88,7 +103,9 @@ "Phone is required": "Telefonnummer er påkrævet", "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", - "Points": "Point", + "Points": "Points", + "Points being calculated": "Point udregnes", + "Points earned prior to May 1, 2021": "Point optjent før 1. maj 2021", "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", "Points needed to level up": "Point nødvendige for at komme i niveau", "Points needed to stay on level": "Point nødvendige for at holde sig på niveau", @@ -96,10 +113,13 @@ "Previous victories": "Tidligere sejre", "Read more": "Læs mere", "Read more about the hotel": "Læs mere om hotellet", + "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Restaurant & Bar": "Restaurant & Bar", "Retype new password": "Gentag den nye adgangskode", "Rooms": "Værelser", "Save": "Gemme", + "Scandic Friends Mastercard": "Scandic Friends Mastercard", + "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Select a country": "Vælg et land", "Select country of residence": "Vælg bopælsland", "Select date of birth": "Vælg fødselsdato", @@ -108,7 +128,10 @@ "Show more": "Vis mere", "Show all amenities": "Vis alle faciliteter", "Skip to main content": "Spring over og gå til hovedindhold", + "Sign up bonus": "Tilmeldingsbonus", "Something went wrong!": "Noget gik galt!", + "Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.", + "Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.", "Street": "Gade", "special character": "speciel karakter", "Total Points": "Samlet antal point", @@ -117,17 +140,22 @@ "Transactions": "Transaktioner", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", "to": "til", + "TUI Points": "TUI-point", "User information": "Brugeroplysninger", "uppercase letter": "stort bogstav", "Visiting address": "Besøgsadresse", + "We could not add a card right now, please try again later.": "Vi kunne ikke tilføje et kort lige nu. Prøv venligst igen senere.", "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Motion", "Where should you go next?": "Find inspiration til dit næste ophold", "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", "Year": "År", + "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", + "Yes, remove my card": "Ja, fjern mit kort", "You have no previous stays.": "Du har ingen tidligere ophold.", "You have no upcoming stays.": "Du har ingen kommende ophold.", + "Your card was successfully removed!": "Dit kort blev fjernet!", "Your card was successfully saved!": "Dit kort blev gemt!", "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your level": "Dit niveau", @@ -136,6 +164,19 @@ "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", "Show map": "Vis kort", + "Check in": "Check ind", + "Check out": "Check ud", + "Summary": "Opsummering", + "Thank you": "Tak", + "We look forward to your visit!": "Vi ser frem til dit besøg!", + "We have sent a detailed confirmation of your booking to your email:": "Vi har sendt en detaljeret bekræftelse af din booking til din email:", + "Download the Scandic app": "Download Scandic-appen", + "View your booking": "Se din booking", + "At latest": "Senest", + "Type of room": "Værelsestype", + "Type of bed": "Sengtype", + "Weekdays": "Hverdage", + "Weekends": "Weekender", "Where to": "Hvorhen", "When": "Hvornår", "Rooms & Guests": "Værelser & gæster", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ca524c900..074cc7588 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -6,8 +6,10 @@ "All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet", "Already a friend?": "Sind wir schon Freunde?", "Amenities": "Annehmlichkeiten", + "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.", + "Are you sure you want to remove the card ending with": "Möchten Sie die Karte mit der Endung", "Arrival date": "Ankunftsdatum", - "as of today": "Ab heute", + "as of today": "Stand heute", "As our": "Als unser", "As our Close Friend": "Als unser enger Freund", "At the hotel": "Im Hotel", @@ -30,6 +32,7 @@ "Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.", "Country": "Land", "Country code": "Landesvorwahl", + "Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht", "Your current level": "Ihr aktuelles Level", "Current password": "Aktuelles Passwort", "characters": "figuren", @@ -41,13 +44,24 @@ "Edit": "Bearbeiten", "Edit profile": "Profil bearbeiten", "Email": "Email", + "Extras to your booking": "Extras zu Ihrer Buchung", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", + "Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.", "Find booking": "Buchung finden", "Flexibility": "Flexibilität", + "Former Scandic Hotel": "Ehemaliges Scandic Hotel", "From": "Fromm", + "from your member profile?": "wirklich aus Ihrem Mitgliedsprofil entfernen?", "Get inspired": "Lassen Sie sich inspieren", "Go back to overview": "Zurück zur Übersicht", + "Level 1": "Level 1", + "Level 2": "Level 2", + "Level 3": "Level 3", + "Level 4": "Level 4", + "Level 5": "Level 5", + "Level 6": "Level 6", + "Level 7": "Level 7", "Highest level": "Höchstes Level", "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", @@ -71,6 +85,7 @@ "Next": "Nächste", "next level:": "Nächstes Level:", "No content published": "Kein Inhalt veröffentlicht", + "No, keep card": "Nein, Karte behalten", "No transactions available": "Keine Transaktionen verfügbar", "Not found": "Nicht gefunden", "night": "nacht", @@ -86,15 +101,20 @@ "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", "Points": "Punkte", + "Points being calculated": "Punkte werden berechnet", + "Points earned prior to May 1, 2021": "Vor dem 1. Mai 2021 gesammelte Punkte", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", "Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden", - "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Niveau zu bleiben", + "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", "spendable points expiring by": "Einlösbare punkte verfallen bis zum", "Previous victories": "Bisherige Siege", "Read more": "Mehr lesen", "Read more about the hotel": "Lesen Sie mehr über das Hotel", + "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", "Retype new password": "Neues Passwort erneut eingeben", "Save": "Speichern", + "Scandic Friends Mastercard": "Scandic Friends Mastercard", + "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Select a country": "Wähle ein Land", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select date of birth": "Geburtsdatum auswählen", @@ -103,25 +123,33 @@ "Show more": "Mehr anzeigen", "Show all amenities": "Alle Annehmlichkeiten anzeigen", "Skip to main content": "Direkt zum Inhalt", + "Sign up bonus": "Anmeldebonus", "Something went wrong!": "Etwas ist schief gelaufen!", + "Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.", + "Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.", "Street": "Straße", "special character": "sonderzeichen", "Total Points": "Gesamtpunktzahl", - "Your points to spend": "Deine Punkte", + "Your points to spend": "Meine Punkte", "Transaction date": "Transaktionsdatum", "Transactions": "Transaktionen", "Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)", "to": "zu", + "TUI Points": "TUI Punkte", "User information": "Nutzerinformation", "uppercase letter": "großbuchstabe", "Visiting address": "Besuchsadresse", + "We could not add a card right now, please try again later.": "Wir konnten momentan keine Karte hinzufügen. Bitte versuchen Sie es später noch einmal.", "Welcome to": "Willkommen zu", "Welcome": "Willkommen", "Where should you go next?": "Wo geht es als Nächstes hin?", "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", "Year": "Jahr", + "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", + "Yes, remove my card": "Ja, meine Karte entfernen", "You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.", "You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.", + "Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!", "Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!", "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your level": "Dein level", @@ -130,6 +158,19 @@ "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", "Show map": "Karte anzeigen", + "Check in": "Einchecken", + "Check out": "Auschecken", + "Summary": "Zusammenfassung", + "Thank you": "Danke", + "We look forward to your visit!": "Wir freuen uns auf Ihren Besuch!", + "We have sent a detailed confirmation of your booking to your email:": "Wir haben eine detaillierte Bestätigung Ihrer Buchung an Ihre E-Mail gesendet:", + "Download the Scandic app": "Laden Sie die Scandic-App herunter", + "View your booking": "Ihre Buchung ansehen", + "At latest": "Spätestens", + "Type of room": "Zimmerart", + "Type of bed": "Bettentyp", + "Weekdays": "Wochentage", + "Weekends": "Wochenenden", "Where to": "Wohin", "When": "Wann", "Rooms & Guests": "Zimmer & Gäste", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 1cf4c1c46..93b93b252 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "All rooms comes with standard amenities", "Already a friend?": "Already a friend?", "Amenities": "Amenities", + "An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.", + "Are you sure you want to remove the card ending with": "Are you sure you want to remove the card ending with", "Arrival date": "Arrival date", "as of today": "as of today", "As our": "As our", @@ -34,6 +36,7 @@ "Your current level": "Your current level", "Current password": "Current password", "characters": "characters", + "Credit card deleted successfully": "Credit card deleted successfully", "Date of Birth": "Date of Birth", "Day": "Day", "Description": "Description", @@ -45,15 +48,26 @@ "Email": "Email", "There are no transactions to display": "There are no transactions to display", "Explore all levels and benefits": "Explore all levels and benefits", + "Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.", + "Extras to your booking": "Extras to your booking", "FAQ": "FAQ", "Find booking": "Find booking", + "Former Scandic Hotel": "Former Scandic Hotel", "Flexibility": "Flexibility", "From": "From", + "from your member profile?": "from your member profile?", "Get inspired": "Get inspired", "Go back to overview": "Go back to overview", "hotelPages.rooms.roomCard.person": "person", "hotelPages.rooms.roomCard.persons": "persons", "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", + "Level 1": "Level 1", + "Level 2": "Level 2", + "Level 3": "Level 3", + "Level 4": "Level 4", + "Level 5": "Level 5", + "Level 6": "Level 6", + "Level 7": "Level 7", "Highest level": "Highest level", "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", @@ -78,6 +92,7 @@ "Next": "Next", "next level:": "next level:", "No content published": "No content published", + "No, keep card": "No, keep card", "No transactions available": "No transactions available", "Not found": "Not found", "night": "night", @@ -94,6 +109,8 @@ "Phone number": "Phone number", "Please enter a valid phone number": "Please enter a valid phone number", "Points": "Points", + "Points being calculated": "Points being calculated", + "Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021", "Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.", "Points needed to level up": "Points needed to level up", "Points needed to stay on level": "Points needed to stay on level", @@ -101,11 +118,14 @@ "Previous victories": "Previous victories", "Read more": "Read more", "Read more about the hotel": "Read more about the hotel", + "Remove card from member profile": "Remove card from member profile", "Restaurant & Bar": "Restaurant & Bar", "Retype new password": "Retype new password", "Rooms": "Rooms", "Save": "Save", "See room details": "See room details", + "Scandic Friends Mastercard": "Scandic Friends Mastercard", + "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Select a country": "Select a country", "Select country of residence": "Select country of residence", "Select date of birth": "Select date of birth", @@ -114,26 +134,34 @@ "Show more": "Show more", "Show all amenities": "Show all amenities", "Skip to main content": "Skip to main content", + "Sign up bonus": "Sign up bonus", "Something went wrong!": "Something went wrong!", + "Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.", + "Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.", "Street": "Street", "special character": "special character", "Total Points": "Total Points", "Your points to spend": "Your points to spend", + "You canceled adding a new credit card.": "You canceled adding a new credit card.", "Transaction date": "Transaction date", "Transactions": "Transactions", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", "to": "to", + "TUI Points": "TUI Points", "User information": "User information", "uppercase letter": "uppercase letter", "Welcome": "Welcome", "Visiting address": "Visiting address", + "We could not add a card right now, please try again later.": "We could not add a card right now, please try again later.", "Welcome to": "Welcome to", "Wellness & Exercise": "Wellness & Exercise", "Where should you go next?": "Where should you go next?", "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", + "Yes, remove my card": "Yes, remove my card", "You have no previous stays.": "You have no previous stays.", "You have no upcoming stays.": "You have no upcoming stays.", + "Your card was successfully removed!": "Your card was successfully removed!", "Your card was successfully saved!": "Your card was successfully saved!", "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your level": "Your level", @@ -142,6 +170,19 @@ "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", "Show map": "Show map", + "Check in": "Check in", + "Check out": "Check out", + "Summary": "Summary", + "Thank you": "Thank you", + "We look forward to your visit!": "We look forward to your visit!", + "We have sent a detailed confirmation of your booking to your email: ": "We have sent a detailed confirmation of your booking to your email: ", + "Download the Scandic app": "Download the Scandic app", + "View your booking": "View your booking", + "At latest": "At latest", + "Type of room": "Type of room", + "Type of bed": "Type of bed", + "Weekdays": "Weekdays", + "Weekends": "Weekends", "Where to": "Where to", "When": "When", "Rooms & Guests": "Rooms & Guests", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 452693de1..a7fac093e 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet", "Already a friend?": "Oletko jo ystävä?", "Amenities": "Mukavuudet", + "An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.", + "Are you sure you want to remove the card ending with": "Haluatko varmasti poistaa kortin, joka päättyy numeroon", "Arrival date": "Saapumispäivä", "as of today": "tästä päivästä lähtien", "As our": "Kuin meidän", @@ -31,6 +33,7 @@ "Could not find requested resource": "Pyydettyä resurssia ei löytynyt", "Country": "Maa", "Country code": "Maatunnus", + "Credit card deleted successfully": "Luottokortti poistettu onnistuneesti", "Your current level": "Nykyinen tasosi", "Current password": "Nykyinen salasana", "characters": "hahmoja", @@ -42,13 +45,24 @@ "Edit": "Muokata", "Edit profile": "Muokkaa profiilia", "Email": "Sähköposti", + "Extras to your booking": "Lisävarusteet varaukseesi", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", + "Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.", "Find booking": "Etsi varaus", "Flexibility": "Joustavuus", + "Former Scandic Hotel": "Entinen Scandic Hotel", "From": "From", + "from your member profile?": "jäsenprofiilistasi?", "Get inspired": "Inspiroidu", "Go back to overview": "Palaa yleiskatsaukseen", + "Level 1": "Taso 1", + "Level 2": "Taso 2", + "Level 3": "Taso 3", + "Level 4": "Taso 4", + "Level 5": "Taso 5", + "Level 6": "Taso 6", + "Level 7": "Taso 7", "Highest level": "Korkein taso", "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", @@ -73,6 +87,7 @@ "Next": "Seuraava", "next level:": "Seuraava taso:", "No content published": "Ei julkaistua sisältöä", + "No, keep card": "Ei, pidä kortti", "No transactions available": "Ei tapahtumia saatavilla", "Not found": "Ei löydetty", "night": "yö", @@ -89,6 +104,8 @@ "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", "Points": "Pistettä", + "Points being calculated": "Pisteitä lasketaan", + "Points earned prior to May 1, 2021": "Ennen 1. toukokuuta 2021 ansaitut pisteet", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", "Points needed to level up": "Pisteitä tarvitaan tasolle pääsemiseksi", "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", @@ -96,10 +113,13 @@ "Previous victories": "Edelliset voitot", "Read more": "Lue lisää", "Read more about the hotel": "Lue lisää hotellista", + "Remove card from member profile": "Poista kortti jäsenprofiilista", "Restaurant & Bar": "Ravintola & Baari", "Retype new password": "Kirjoita uusi salasana uudelleen", "Rooms": "Huoneet", "Save": "Tallentaa", + "Scandic Friends Mastercard": "Scandic Friends Mastercard", + "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Select a country": "Valitse maa", "Select country of residence": "Valitse asuinmaa", "Select date of birth": "Valitse syntymäaika", @@ -108,7 +128,10 @@ "Show more": "Näytä lisää", "Show all amenities": "Näytä kaikki mukavuudet", "Skip to main content": "Siirry pääsisältöön", + "Sign up bonus": "Rekisteröidy bonus", "Something went wrong!": "Jotain meni pieleen!", + "Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.", + "Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.", "Street": "Katu", "special character": "erikoishahmo", "Total Points": "Kokonaispisteet", @@ -117,18 +140,23 @@ "Transactions": "Tapahtumat", "Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)", "to": "to", + "TUI Points": "TUI-pisteet", "User information": "Käyttäjän tiedot", "uppercase letter": "iso kirjain", "Visiting address": "Käyntiosoite", + "We could not add a card right now, please try again later.": "Emme voineet lisätä korttia juuri nyt. Yritä myöhemmin uudelleen.", "Welcome": "Tervetuloa", "Welcome to": "Tervetuloa", "Wellness & Exercise": "Hyvinvointi & Liikunta", "Where should you go next?": "Mihin menisit seuraavaksi?", "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", "Year": "Vuosi", + "Yes, remove my card": "Kyllä, poista korttini", "You have no previous stays.": "Sinulla ei ole aiempaa oleskelua.", "You have no upcoming stays.": "Sinulla ei ole tulevia oleskeluja.", + "Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!", "Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!", + "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", "Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!", "Your level": "Tasosi", "Zip code": "Postinumero", @@ -136,6 +164,19 @@ "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", "Show map": "Näytä kartta", + "Check in": "Sisäänkirjautuminen", + "Check out": "Uloskirjautuminen", + "Summary": "Yhteenveto", + "Thank you": "Kiitos", + "We look forward to your visit!": "Odotamme innolla vierailuasi!", + "We have sent a detailed confirmation of your booking to your email:": "Olemme lähettäneet yksityiskohtaisen varausvahvistuksen sähköpostiisi:", + "Download the Scandic app": "Lataa Scandic-sovellus", + "View your booking": "Näytä varauksesi", + "At latest": "Viimeistään", + "Type of room": "Huonetyyppi", + "Type of bed": "Vuodetyyppi", + "Weekdays": "Arkisin", + "Weekends": "Viikonloppuisin", "Where to": "Minne", "When": "Kun", "Rooms & Guestss": "Huoneet & Vieraat", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index a3e2ce356..e8b6915e9 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -7,12 +7,14 @@ "All rooms comes with standard amenities": "Alle rommene har standard fasiliteter", "Already a friend?": "Allerede Friend?", "Amenities": "Fasiliteter", + "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.", + "Are you sure you want to remove the card ending with": "Er du sikker på at du vil fjerne kortet som slutter på", "Arrival date": "Ankomstdato", "as of today": "per idag", "As our": "Som vår", "As our Close Friend": "Som vår nære venn", "At the hotel": "På hotellet", - "Book": "Bok", + "Book": "Bestill", "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", "by": "innen", @@ -34,6 +36,7 @@ "Your current level": "Ditt nåværende nivå", "Current password": "Nåværende passord", "characters": "tegn", + "Credit card deleted successfully": "Kredittkort slettet", "Date of Birth": "Fødselsdato", "Day": "Dag", "Description": "Beskrivelse", @@ -42,13 +45,24 @@ "Edit": "Redigere", "Edit profile": "Rediger profil", "Email": "E-post", + "Extras to your booking": "Ekstra til din bestilling", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler", + "Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.", "Find booking": "Finn booking", "Flexibility": "Fleksibilitet", + "Former Scandic Hotel": "Tidligere Scandic Hotel", "From": "Fra", + "from your member profile?": "fra medlemsprofilen din?", "Get inspired": "Bli inspirert", "Go back to overview": "Gå tilbake til oversikten", + "Level 1": "Nivå 1", + "Level 2": "Nivå 2", + "Level 3": "Nivå 3", + "Level 4": "Nivå 4", + "Level 5": "Nivå 5", + "Level 6": "Nivå 6", + "Level 7": "Nivå 7", "Highest level": "Høyeste nivå", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", @@ -73,6 +87,7 @@ "Next": "Neste", "next level:": "Neste nivå:", "No content published": "Ingen innhold publisert", + "No, keep card": "Nei, behold kortet", "No transactions available": "Ingen transaksjoner tilgjengelig", "Not found": "Ikke funnet", "night": "natt", @@ -89,6 +104,8 @@ "Phone number": "Telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", "Points": "Poeng", + "Points being calculated": "Poeng beregnes", + "Points earned prior to May 1, 2021": "Poeng opptjent før 1. mai 2021", "Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.", "Points needed to level up": "Poeng som trengs for å komme opp i nivå", "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", @@ -96,10 +113,13 @@ "Previous victories": "Tidligere seire", "Read more": "Les mer", "Read more about the hotel": "Les mer om hotellet", + "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Restaurant & Bar": "Restaurant & Bar", "Retype new password": "Skriv inn nytt passord på nytt", "Rooms": "Rom", "Save": "Lagre", + "Scandic Friends Mastercard": "Scandic Friends Mastercard", + "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Select a country": "Velg et land", "Select country of residence": "Velg bostedsland", "Select date of birth": "Velg fødselsdato", @@ -108,7 +128,10 @@ "Show more": "Vis mer", "Show all amenities": "Vis alle fasiliteter", "Skip to main content": "Gå videre til hovedsiden", + "Sign up bonus": "Registreringsbonus", "Something went wrong!": "Noe gikk galt!", + "Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.", + "Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.", "Street": "Gate", "special character": "spesiell karakter", "Total Points": "Totale poeng", @@ -117,17 +140,22 @@ "Transactions": "Transaksjoner", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", "to": "til", + "TUI Points": "TUI-poeng", "User information": "Brukerinformasjon", "uppercase letter": "stor bokstav", "Visiting address": "Besøksadresse", + "We could not add a card right now, please try again later.": "Vi kunne ikke legge til et kort akkurat nå. Prøv igjen senere.", "Welcome": "Velkommen", "Welcome to": "Velkommen til", "Wellness & Exercise": "Velvære & Trening", "Where should you go next?": "Hvor ønsker du å reise neste gang?", "Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Year": "År", + "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", + "Yes, remove my card": "Ja, fjern kortet mitt", "You have no previous stays.": "Du har ingen tidligere opphold.", "You have no upcoming stays.": "Du har ingen kommende opphold.", + "Your card was successfully removed!": "Kortet ditt ble fjernet!", "Your card was successfully saved!": "Kortet ditt ble lagret!", "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your level": "Ditt nivå", @@ -136,6 +164,19 @@ "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", "Show map": "Vis kart", + "Check in": "Sjekk inn", + "Check out": "Sjekk ut", + "Summary": "Sammendrag", + "Thank you": "Takk", + "We look forward to your visit!": "Vi ser frem til ditt besøk!", + "We have sent a detailed confirmation of your booking to your email:": "Vi har sendt en detaljert bekreftelse av din bestilling til din e-post:", + "Download the Scandic app": "Last ned Scandic-appen", + "View your booking": "Se din bestilling", + "At latest": "Senest", + "Type of room": "Romtype", + "Type of bed": "Sengtype", + "Weekdays": "Hverdager", + "Weekends": "Helger", "Where to": "Hvor skal du", "When": "Når", "Rooms & Guests": "Rom og gjester", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 0b26bc9b4..b40f77323 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -7,6 +7,8 @@ "All rooms comes with standard amenities": "Alla rum har standardbekvämligheter", "Already a friend?": "Är du redan en vän?", "Amenities": "Bekvämligheter", + "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.", + "Are you sure you want to remove the card ending with": "Är du säker på att du vill ta bort kortet som slutar med", "Arrival date": "Ankomstdatum", "as of today": "från och med idag", "As our": "Som vår", @@ -31,6 +33,7 @@ "Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Country": "Land", "Country code": "Landskod", + "Credit card deleted successfully": "Kreditkort har tagits bort", "Your current level": "Din nuvarande nivå", "Current password": "Nuvarande lösenord", "characters": "tecken", @@ -42,13 +45,24 @@ "Edit": "Redigera", "Edit profile": "Redigera profil", "Email": "E-post", + "Extras to your booking": "Extra till din bokning", "There are no transactions to display": "Det finns inga transaktioner att visa", "Explore all levels and benefits": "Utforska alla nivåer och fördelar", + "Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.", "Find booking": "Hitta bokning", "Flexibility": "Flexibilitet", + "Former Scandic Hotel": "Tidigare Scandic Hotel", "From": "Från", + "from your member profile?": "från din medlemsprofil?", "Get inspired": "Bli inspirerad", "Go back to overview": "Gå tillbaka till översikten", + "Level 1": "Nivå 1", + "Level 2": "Nivå 2", + "Level 3": "Nivå 3", + "Level 4": "Nivå 4", + "Level 5": "Nivå 5", + "Level 6": "Nivå 6", + "Level 7": "Nivå 7", "Highest level": "Högsta nivå", "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", @@ -76,6 +90,7 @@ "Next": "Nästa", "next level:": "Nästa nivå:", "No content published": "Inget innehåll publicerat", + "No, keep card": "Nej, behåll kortet", "No transactions available": "Inga transaktioner tillgängliga", "Not found": "Hittades inte", "night": "natt", @@ -92,6 +107,8 @@ "Phone number": "Telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", "Points": "Poäng", + "Points being calculated": "Poäng beräknas", + "Points earned prior to May 1, 2021": "Poäng intjänade före 1 maj 2021", "Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.", "Points needed to level up": "Poäng som behövs för att gå upp i nivå", "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", @@ -99,10 +116,13 @@ "Previous victories": "Tidigare segrar", "Read more": "Läs mer", "Read more about the hotel": "Läs mer om hotellet", + "Remove card from member profile": "Ta bort kortet från medlemsprofilen", "Restaurant & Bar": "Restaurang & Bar", "Retype new password": "Upprepa nytt lösenord", "Rooms": "Rum", "Save": "Spara", + "Scandic Friends Mastercard": "Scandic Friends Mastercard", + "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Select a country": "Välj ett land", "Select country of residence": "Välj bosättningsland", "Select date of birth": "Välj födelsedatum", @@ -111,7 +131,10 @@ "Show more": "Visa mer", "Show all amenities": "Visa alla bekvämligheter", "Skip to main content": "Fortsätt till huvudinnehåll", + "Sign up bonus": "Registreringsbonus", "Something went wrong!": "Något gick fel!", + "Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.", + "Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.", "Street": "Gata", "special character": "speciell karaktär", "Total Points": "Poäng totalt", @@ -120,16 +143,21 @@ "Transactions": "Transaktioner", "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", "to": "till", + "TUI Points": "TUI-poäng", "User information": "Användar information", "uppercase letter": "stor bokstav", "Visiting address": "Besöksadress", + "We could not add a card right now, please try again later.": "Vi kunde inte lägga till ett kort just nu, vänligen försök igen senare.", "Welcome": "Välkommen", "Wellness & Exercise": "Hälsa & Träning", "Where should you go next?": "Låter inte en spontanweekend härligt?", "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", "Year": "År", + "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", + "Yes, remove my card": "Ja, ta bort mitt kort", "You have no previous stays.": "Du har inga tidigare vistelser.", "You have no upcoming stays.": "Du har inga planerade resor.", + "Your card was successfully removed!": "Ditt kort har tagits bort!", "Your card was successfully saved!": "Ditt kort har sparats!", "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your level": "Din nivå", @@ -138,6 +166,19 @@ "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", "Show map": "Visa karta", + "Check in": "Checka in", + "Check out": "Checka ut", + "Summary": "Sammanfattning", + "Thank you": "Tack", + "We look forward to your visit!": "Vi ser fram emot ditt besök!", + "We have sent a detailed confirmation of your booking to your email:": "Vi har skickat en detaljerad bekräftelse av din bokning till din e-post:", + "Download the Scandic app": "Ladda ner Scandic-appen", + "View your booking": "Visa din bokning", + "At latest": "Senast", + "Type of room": "Rumstyp", + "Type of bed": "Sängtyp", + "Weekdays": "Vardagar", + "Weekends": "Helger", "Where to": "Vart", "When": "När", "Rooms & Guests": "Rum och gäster", diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 7235b9df9..7fb7e5e9f 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -13,6 +13,8 @@ export namespace endpoints { upcomingStays = "booking/v1/Stays/future", previousStays = "booking/v1/Stays/past", hotels = "hotel/v1/Hotels", + intiateSaveCard = `${creditCards}/initiateSaveCard`, + deleteCreditCard = `${profile}/creditCards`, } } diff --git a/lib/api/index.ts b/lib/api/index.ts index 97d24039b..a3ec13a6c 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -27,7 +27,7 @@ const fetch = fetchRetry(global.fetch, { }) export async function get( - endpoint: Endpoint | `${endpoints.v1.hotels}/${string}`, + endpoint: Endpoint | `${Endpoint}/${string}`, options: RequestOptionsWithOutBody, params?: URLSearchParams ) { @@ -38,7 +38,7 @@ export async function get( } export async function patch( - endpoint: Endpoint, + endpoint: Endpoint | `${Endpoint}/${string}`, options: RequestOptionsWithJSONBody ) { const { body, ...requestOptions } = options @@ -54,11 +54,12 @@ export async function patch( export async function post( endpoint: Endpoint | `${Endpoint}/${string}`, - options: RequestOptionsWithJSONBody + options: RequestOptionsWithJSONBody, + params?: URLSearchParams ) { const { body, ...requestOptions } = options return fetch( - `${env.API_BASEURL}/${endpoint}`, + `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`, merge.all([ defaultOptions, { body: JSON.stringify(body), method: "POST" }, @@ -68,11 +69,12 @@ export async function post( } export async function remove( - endpoint: Endpoint, - options: RequestOptionsWithOutBody + endpoint: Endpoint | `${Endpoint}/${string}`, + options: RequestOptionsWithOutBody, + params?: URLSearchParams ) { return fetch( - `${env.API_BASEURL}/${endpoint}`, + `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`, merge.all([defaultOptions, { method: "DELETE" }, options]) ) } diff --git a/lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql b/lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql new file mode 100644 index 000000000..036889c24 --- /dev/null +++ b/lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql @@ -0,0 +1,37 @@ +fragment ContentPageBreadcrumbs on ContentPage { + web { + breadcrumbs { + title + parentsConnection { + edges { + node { + ... on ContentPage { + web { + breadcrumbs { + title + } + } + system { + locale + uid + } + url + } + ... on LoyaltyPage { + web { + breadcrumbs { + title + } + } + system { + locale + uid + } + url + } + } + } + } + } + } +} diff --git a/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql b/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql new file mode 100644 index 000000000..a169b5182 --- /dev/null +++ b/lib/graphql/Fragments/LoyaltyPage/MetaData.graphql @@ -0,0 +1,20 @@ +#import "../Image.graphql" + +fragment LoyaltyPageMetaData on LoyaltyPage { + web { + seo_metadata { + title + description + imageConnection { + edges { + node { + ...Image + } + } + } + } + breadcrumbs { + title + } + } +} diff --git a/lib/graphql/Fragments/MyPages/MetaData.graphql b/lib/graphql/Fragments/MyPages/MetaData.graphql new file mode 100644 index 000000000..644594401 --- /dev/null +++ b/lib/graphql/Fragments/MyPages/MetaData.graphql @@ -0,0 +1,20 @@ +#import "../Image.graphql" + +fragment MyPagesMetaData on AccountPage { + web { + seo_metadata { + title + description + imageConnection { + edges { + node { + ...Image + } + } + } + } + breadcrumbs { + title + } + } +} diff --git a/lib/graphql/Fragments/Refs/ContentPage/Breadcrumbs.graphql b/lib/graphql/Fragments/Refs/ContentPage/Breadcrumbs.graphql new file mode 100644 index 000000000..b3c887a34 --- /dev/null +++ b/lib/graphql/Fragments/Refs/ContentPage/Breadcrumbs.graphql @@ -0,0 +1,38 @@ +#import "../System.graphql" + +fragment ContentPageBreadcrumbsRefs on ContentPage { + web { + breadcrumbs { + title + parentsConnection { + edges { + node { + ... on ContentPage { + web { + breadcrumbs { + title + } + } + system { + ...System + } + } + ... on LoyaltyPage { + web { + breadcrumbs { + title + } + } + system { + ...System + } + } + } + } + } + } + } + system { + ...System + } +} diff --git a/lib/graphql/Fragments/Refs/ContentPage.graphql b/lib/graphql/Fragments/Refs/ContentPage/ContentPage.graphql similarity index 72% rename from lib/graphql/Fragments/Refs/ContentPage.graphql rename to lib/graphql/Fragments/Refs/ContentPage/ContentPage.graphql index daf7471a3..80edfcbca 100644 --- a/lib/graphql/Fragments/Refs/ContentPage.graphql +++ b/lib/graphql/Fragments/Refs/ContentPage/ContentPage.graphql @@ -1,4 +1,4 @@ -#import "./System.graphql" +#import "../System.graphql" fragment ContentPageRef on ContentPage { system { diff --git a/lib/graphql/Query/AccountPage.graphql b/lib/graphql/Query/AccountPage.graphql index b21150bee..538b7e64a 100644 --- a/lib/graphql/Query/AccountPage.graphql +++ b/lib/graphql/Query/AccountPage.graphql @@ -3,7 +3,7 @@ #import "../Fragments/MyPages/AccountPage/AccountPageContentTextContent.graphql" #import "../Fragments/Refs/MyPages/AccountPage.graphql" -#import "../Fragments/Refs/ContentPage.graphql" +#import "../Fragments/Refs/ContentPage/ContentPage.graphql" #import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql" #import "../Fragments/Refs/System.graphql" diff --git a/lib/graphql/Query/BreadcrumbsContentPage.graphql b/lib/graphql/Query/BreadcrumbsContentPage.graphql new file mode 100644 index 000000000..fddbffc09 --- /dev/null +++ b/lib/graphql/Query/BreadcrumbsContentPage.graphql @@ -0,0 +1,21 @@ +#import "../Fragments/ContentPage/Breadcrumbs.graphql" +#import "../Fragments/Refs/ContentPage/Breadcrumbs.graphql" + +query GetContentPageBreadcrumbs($locale: String!, $url: String!) { + all_content_page(locale: $locale, where: { url: $url }) { + items { + ...ContentPageBreadcrumbs + system { + uid + } + } + } +} + +query GetContentPageBreadcrumbsRefs($locale: String!, $url: String!) { + all_content_page(locale: $locale, where: { url: $url }) { + items { + ...ContentPageBreadcrumbsRefs + } + } +} diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql new file mode 100644 index 000000000..dee4d9610 --- /dev/null +++ b/lib/graphql/Query/ContentPage.graphql @@ -0,0 +1,52 @@ +query GetContentPage($locale: String!, $uid: String!) { + content_page(uid: $uid, locale: $locale) { + title + header { + heading + preamble + } + hero_image + system { + uid + created_at + updated_at + locale + } + } +} + +query GetDaDeEnUrlsContentPage($uid: String!) { + de: all_content_page(where: { uid: $uid }, locale: "de") { + items { + url + } + } + en: all_content_page(where: { uid: $uid }, locale: "en") { + items { + url + } + } + da: all_content_page(where: { uid: $uid }, locale: "da") { + items { + url + } + } +} + +query GetFiNoSvUrlsContentPage($uid: String!) { + fi: all_content_page(where: { uid: $uid }, locale: "fi") { + items { + url + } + } + no: all_content_page(where: { uid: $uid }, locale: "no") { + items { + url + } + } + sv: all_content_page(where: { uid: $uid }, locale: "sv") { + items { + url + } + } +} diff --git a/lib/graphql/Query/LoyaltyPage.graphql b/lib/graphql/Query/LoyaltyPage.graphql index b0cd9cb29..15900cb3e 100644 --- a/lib/graphql/Query/LoyaltyPage.graphql +++ b/lib/graphql/Query/LoyaltyPage.graphql @@ -5,13 +5,12 @@ #import "../Fragments/Blocks/Refs/Card.graphql" #import "../Fragments/Blocks/Refs/LoyaltyCard.graphql" -#import "../Fragments/LoyaltyPage/Breadcrumbs.graphql" #import "../Fragments/PageLink/AccountPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" #import "../Fragments/Refs/MyPages/AccountPage.graphql" -#import "../Fragments/Refs/ContentPage.graphql" +#import "../Fragments/Refs/ContentPage/ContentPage.graphql" #import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql" #import "../Fragments/Refs/System.graphql" @@ -107,6 +106,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) { } title heading + preamble + hero_image sidebar { __typename ... on LoyaltyPageSidebarDynamicContent { @@ -167,7 +168,6 @@ query GetLoyaltyPage($locale: String!, $uid: String!) { } } } - ...LoyaltyPageBreadcrumbs system { uid created_at diff --git a/lib/graphql/Query/MetaDataLoyaltyPage.graphql b/lib/graphql/Query/MetaDataLoyaltyPage.graphql new file mode 100644 index 000000000..9b6ba5d77 --- /dev/null +++ b/lib/graphql/Query/MetaDataLoyaltyPage.graphql @@ -0,0 +1,12 @@ +#import "../Fragments/LoyaltyPage/MetaData.graphql" + +query GetLoyaltyPageMetaData($locale: String!, $url: String!) { + all_loyalty_page(locale: $locale, where: { url: $url }) { + items { + ...LoyaltyPageMetaData + system { + uid + } + } + } +} diff --git a/lib/graphql/Query/MetaDataMyPages.graphql b/lib/graphql/Query/MetaDataMyPages.graphql new file mode 100644 index 000000000..14e255f02 --- /dev/null +++ b/lib/graphql/Query/MetaDataMyPages.graphql @@ -0,0 +1,12 @@ +#import "../Fragments/MyPages/MetaData.graphql" + +query GetMyPagesMetaData($locale: String!, $url: String!) { + all_account_page(locale: $locale, where: { url: $url }) { + items { + ...MyPagesMetaData + system { + uid + } + } + } +} diff --git a/lib/graphql/Query/NavigationMyPages.graphql b/lib/graphql/Query/NavigationMyPages.graphql index b4708a56a..c0f8acb44 100644 --- a/lib/graphql/Query/NavigationMyPages.graphql +++ b/lib/graphql/Query/NavigationMyPages.graphql @@ -2,7 +2,7 @@ #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" #import "../Fragments/Refs/MyPages/AccountPage.graphql" -#import "../Fragments/Refs/ContentPage.graphql" +#import "../Fragments/Refs/ContentPage/ContentPage.graphql" #import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql" #import "../Fragments/Refs/System.graphql" diff --git a/lib/graphql/_request.ts b/lib/graphql/_request.ts index 9dad21077..b3253f26b 100644 --- a/lib/graphql/_request.ts +++ b/lib/graphql/_request.ts @@ -50,9 +50,9 @@ export async function request( const nr = Math.random() console.log(`START REQUEST ${nr}`) console.time(`OUTGOING REQUEST ${nr}`) - console.log(`Sending reqeust to ${env.CMS_URL}`) - console.log(`Query:`, print(query as DocumentNode)) - console.log(`Variables:`, variables) + // console.log(`Sending reqeust to ${env.CMS_URL}`) + // console.log(`Query:`, print(query as DocumentNode)) + // console.log(`Variables:`, variables) const response = await client.request({ document: query, @@ -64,7 +64,7 @@ export async function request( }) console.timeEnd(`OUTGOING REQUEST ${nr}`) - console.log({ response }) + // console.log({ response }) return { data: response } } catch (error) { diff --git a/lib/trpc/client.ts b/lib/trpc/client.ts index d3ba9ef68..46d50e91a 100644 --- a/lib/trpc/client.ts +++ b/lib/trpc/client.ts @@ -1,5 +1,9 @@ import { createTRPCReact } from "@trpc/react-query" +import { inferRouterInputs, inferRouterOutputs } from "@trpc/server" import type { AppRouter } from "@/server" export const trpc = createTRPCReact() + +export type RouterInput = inferRouterInputs +export type RouterOutput = inferRouterOutputs diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index d824755b4..383da4966 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server" import { redirect } from "next/navigation" import { Lang } from "@/constants/languages" +import { login } from "@/constants/routes/handleAuth" import { webviews } from "@/constants/routes/webviews" import { appRouter } from "@/server" import { createContext } from "@/server/context" @@ -13,12 +14,11 @@ const createCaller = createCallerFactory(appRouter) export function serverClient() { return createCaller(createContext(), { onError: ({ ctx, error, input, path, type }) => { - console.error(`Server Client error for ${type}: ${path}`) + console.error(`[serverClient] error for ${type}: ${path}`, error) + if (input) { - console.error(`Received input:`) - console.error(input) + console.error(`[serverClient] received input:`, input) } - console.error(error) if (error instanceof TRPCError) { if (error.code === "UNAUTHORIZED") { @@ -41,12 +41,13 @@ export function serverClient() { redirectUrl ) + console.log(`[serverClient] onError redirecting to: ${redirectUrl}`) redirect(redirectUrl) } - redirect( - `/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}` - ) + const redirectUrl = `${login[lang]}?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}` + console.log(`[serverClient] onError redirecting to: ${redirectUrl}`) + redirect(redirectUrl) } } diff --git a/middlewares/authRequired.ts b/middlewares/authRequired.ts index 222f408fe..0e50330ee 100644 --- a/middlewares/authRequired.ts +++ b/middlewares/authRequired.ts @@ -49,9 +49,9 @@ export const middleware = auth(async (request) => { } const publicUrl = new URL(env.PUBLIC_URL) - const nextUrlClone = nextUrl.clone() - nextUrlClone.host = publicUrl.host - nextUrlClone.hostname = publicUrl.hostname + const nextUrlPublic = nextUrl.clone() + nextUrlPublic.host = publicUrl.host + nextUrlPublic.hostname = publicUrl.hostname /** * Function to validate MFA from token data @@ -67,8 +67,8 @@ export const middleware = auth(async (request) => { if (isLoggedIn && isMFAPath && isMFAInvalid()) { const headers = new Headers(request.headers) - headers.set("x-mfa-login", "true") - headers.set("x-returnurl", nextUrlClone.href) + headers.set("x-returnurl", nextUrlPublic.href) + headers.set("x-login-source", "mfa") return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { request: { headers, @@ -87,13 +87,16 @@ export const middleware = auth(async (request) => { const headers = new Headers() headers.append( "set-cookie", - `redirectTo=${encodeURIComponent(nextUrlClone.href)}; Path=/; HttpOnly; SameSite=Lax` + `redirectTo=${encodeURIComponent(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax` ) const loginUrl = login[lang] - return NextResponse.redirect(new URL(loginUrl, nextUrlClone), { + const redirectUrl = new URL(loginUrl, nextUrlPublic) + const redirectOpts = { headers, - }) + } + console.log(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts) + return NextResponse.redirect(redirectUrl, redirectOpts) }) as NextMiddleware // See comment above export const matcher: MiddlewareMatcher = (request) => { diff --git a/middlewares/currentWebLogin.ts b/middlewares/currentWebLogin.ts index 49c664be3..14fb89e67 100644 --- a/middlewares/currentWebLogin.ts +++ b/middlewares/currentWebLogin.ts @@ -19,6 +19,7 @@ export const middleware: NextMiddleware = (request) => { const headers = new Headers(request.headers) headers.set("x-returnurl", returnUrl) + headers.set("x-login-source", "seamless") return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { request: { diff --git a/middlewares/currentWebLoginEmail.ts b/middlewares/currentWebLoginEmail.ts index 9e27016ed..3f53417df 100644 --- a/middlewares/currentWebLoginEmail.ts +++ b/middlewares/currentWebLoginEmail.ts @@ -19,7 +19,7 @@ export const middleware: NextMiddleware = (request) => { const headers = new Headers(request.headers) headers.set("x-returnurl", returnUrl) - headers.set("x-magic-link", "1") + headers.set("x-login-source", "seamless-magiclink") return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { request: { diff --git a/middlewares/currentWebLogout.ts b/middlewares/currentWebLogout.ts index 43fba7669..c358012f2 100644 --- a/middlewares/currentWebLogout.ts +++ b/middlewares/currentWebLogout.ts @@ -23,6 +23,7 @@ export const middleware: NextMiddleware = (request) => { const headers = new Headers(request.headers) headers.set("x-returnurl", redirectTo) + headers.set("x-logout-source", "seamless") return NextResponse.rewrite(new URL(`/${lang}/logout`, request.nextUrl), { request: { diff --git a/middlewares/myPages.ts b/middlewares/myPages.ts index bf405209c..fcadc8736 100644 --- a/middlewares/myPages.ts +++ b/middlewares/myPages.ts @@ -34,7 +34,9 @@ export const middleware: NextMiddleware = async (request) => { nextUrlClone.hostname = publicUrl.hostname const overviewUrl = overview[lang] - return NextResponse.redirect(new URL(overviewUrl, nextUrlClone)) + const redirectUrl = new URL(overviewUrl, nextUrlClone) + console.log(`[myPages] redirecting to: ${redirectUrl}`) + return NextResponse.redirect(redirectUrl) } const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "") diff --git a/middlewares/utils.ts b/middlewares/utils.ts index a96045ed6..cc437133a 100644 --- a/middlewares/utils.ts +++ b/middlewares/utils.ts @@ -1,3 +1,6 @@ +import { env } from "@/env/server" +import { internalServerError } from "@/server/errors/next" + import { findLang } from "@/utils/languages" import { removeTrailingSlash } from "@/utils/url" @@ -6,6 +9,17 @@ 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 headers = new Headers(request.headers) headers.set("x-lang", lang) headers.set( @@ -14,7 +28,7 @@ export function getDefaultRequestHeaders(request: NextRequest) { request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "") ) ) - headers.set("x-url", removeTrailingSlash(request.nextUrl.href)) + headers.set("x-url", removeTrailingSlash(nextUrl.href)) return headers } diff --git a/server/routers/contentstack/breadcrumbs/output.ts b/server/routers/contentstack/breadcrumbs/output.ts index f8e775b90..2fca07734 100644 --- a/server/routers/contentstack/breadcrumbs/output.ts +++ b/server/routers/contentstack/breadcrumbs/output.ts @@ -57,7 +57,15 @@ export const validateLoyaltyPageBreadcrumbsRefsContentstackSchema = z.object({ }) export type GetLoyaltyPageBreadcrumbsRefsData = z.infer< - typeof validateLoyaltyPageBreadcrumbsContentstackSchema + typeof validateLoyaltyPageBreadcrumbsRefsContentstackSchema +> + +export const validateContentPageBreadcrumbsRefsContentstackSchema = z.object({ + all_content_page: breadcrumbsRefsItems, +}) + +export type GetContentPageBreadcrumbsRefsData = z.infer< + typeof validateContentPageBreadcrumbsRefsContentstackSchema > const page = z.object({ @@ -110,3 +118,11 @@ export const validateLoyaltyPageBreadcrumbsContentstackSchema = z.object({ export type GetLoyaltyPageBreadcrumbsData = z.infer< typeof validateLoyaltyPageBreadcrumbsContentstackSchema > + +export const validateContentPageBreadcrumbsContentstackSchema = z.object({ + all_content_page: breadcrumbsItems, +}) + +export type GetContentPageBreadcrumbsData = z.infer< + typeof validateContentPageBreadcrumbsContentstackSchema +> diff --git a/server/routers/contentstack/breadcrumbs/query.ts b/server/routers/contentstack/breadcrumbs/query.ts index 0379c05ee..113f0430f 100644 --- a/server/routers/contentstack/breadcrumbs/query.ts +++ b/server/routers/contentstack/breadcrumbs/query.ts @@ -1,3 +1,7 @@ +import { + GetContentPageBreadcrumbs, + GetContentPageBreadcrumbsRefs, +} from "@/lib/graphql/Query/BreadcrumbsContentPage.graphql" import { GetLoyaltyPageBreadcrumbs, GetLoyaltyPageBreadcrumbsRefs, @@ -9,10 +13,14 @@ import { import { contentstackExtendedProcedureUID, router } from "@/server/trpc" import { + type GetContentPageBreadcrumbsData, + type GetContentPageBreadcrumbsRefsData, type GetLoyaltyPageBreadcrumbsData, type GetLoyaltyPageBreadcrumbsRefsData, type GetMyPagesBreadcrumbsData, type GetMyPagesBreadcrumbsRefsData, + validateContentPageBreadcrumbsContentstackSchema, + validateContentPageBreadcrumbsRefsContentstackSchema, validateLoyaltyPageBreadcrumbsContentstackSchema, validateLoyaltyPageBreadcrumbsRefsContentstackSchema, validateMyPagesBreadcrumbsContentstackSchema, @@ -76,6 +84,54 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) { ) } +async function getContentPageBreadcrumbs(variables: Variables) { + const refsResponse = await getRefsResponse( + GetContentPageBreadcrumbsRefs, + variables + ) + + const validatedRefsData = + validateContentPageBreadcrumbsRefsContentstackSchema.safeParse( + refsResponse.data + ) + + if (!validatedRefsData.success) { + console.error( + `Failed to validate Contentpage Breadcrumbs Refs - (url: ${variables.url})` + ) + console.error(validatedRefsData.error) + return null + } + + const tags = getTags(validatedRefsData.data.all_content_page, variables) + + const response = await getResponse( + GetContentPageBreadcrumbs, + variables, + tags + ) + + if (!response.data.all_content_page.items[0].web?.breadcrumbs?.title) { + return null + } + + const validatedBreadcrumbsData = + validateContentPageBreadcrumbsContentstackSchema.safeParse(response.data) + + if (!validatedBreadcrumbsData.success) { + console.error( + `Failed to validate Contentpage Breadcrumbs Data - (url: ${variables.url})` + ) + console.error(validatedBreadcrumbsData.error) + return null + } + + return getBreadcrumbs( + validatedBreadcrumbsData.data.all_content_page.items[0], + variables.locale + ) +} + async function getMyPagesBreadcrumbs(variables: Variables) { const refsResponse = await getRefsResponse( GetMyPagesBreadcrumbsRefs, @@ -133,6 +189,8 @@ export const breadcrumbsQueryRouter = router({ switch (ctx.contentType) { case PageTypeEnum.accountPage: return await getMyPagesBreadcrumbs(variables) + case PageTypeEnum.contentPage: + return await getContentPageBreadcrumbs(variables) case PageTypeEnum.loyaltyPage: return await getLoyaltyPageBreadcrumbs(variables) default: diff --git a/server/routers/contentstack/contentPage/index.ts b/server/routers/contentstack/contentPage/index.ts new file mode 100644 index 000000000..52130d7b4 --- /dev/null +++ b/server/routers/contentstack/contentPage/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { contentPageQueryRouter } from "./query" + +export const contentPageRouter = mergeRouters(contentPageQueryRouter) diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts new file mode 100644 index 000000000..134a179f1 --- /dev/null +++ b/server/routers/contentstack/contentPage/output.ts @@ -0,0 +1,22 @@ +import { z } from "zod" + +import { Lang } from "@/constants/languages" + +import { imageVaultAssetSchema } from "../schemas/imageVault" + +export const validateContentPageSchema = z.object({ + content_page: z.object({ + title: z.string(), + header: z.object({ + heading: z.string(), + preamble: z.string(), + }), + hero_image: imageVaultAssetSchema.nullable().optional(), + system: z.object({ + uid: z.string(), + locale: z.nativeEnum(Lang), + created_at: z.string(), + updated_at: z.string(), + }), + }), +}) diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts new file mode 100644 index 000000000..4cb632785 --- /dev/null +++ b/server/routers/contentstack/contentPage/query.ts @@ -0,0 +1,73 @@ +import { Lang } from "@/constants/languages" +import { GetContentPage } from "@/lib/graphql/Query/ContentPage.graphql" +import { request } from "@/lib/graphql/request" +import { notFound } from "@/server/errors/trpc" +import { contentstackExtendedProcedureUID, router } from "@/server/trpc" + +import { generateTag } from "@/utils/generateTag" +import { makeImageVaultImage } from "@/utils/imageVault" + +import { validateContentPageSchema } from "./output" + +import { + TrackingChannelEnum, + TrackingSDKPageData, +} from "@/types/components/tracking" +import { + ContentPage, + ContentPageDataRaw, +} from "@/types/trpc/routers/contentstack/contentPage" + +export const contentPageQueryRouter = router({ + get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { + const { lang, uid } = ctx + + // TODO: Refs request should be done when adding more data to this query + // which has references to other pages. + + const response = await request( + GetContentPage, + { + locale: lang, + uid, + }, + { tags: [generateTag(lang, uid)] } + ) + + if (!response.data) { + throw notFound(response) + } + + const validatedContentPage = validateContentPageSchema.safeParse( + response.data + ) + + if (!validatedContentPage.success) { + console.error( + `Failed to validate Contentpage Data - (lang: ${lang}, uid: ${uid})` + ) + console.error(validatedContentPage.error) + return null + } + + const contentPageData = validatedContentPage.data.content_page + const contentPage: ContentPage = { + ...contentPageData, + heroImage: makeImageVaultImage(contentPageData.hero_image), + } + + const tracking: TrackingSDKPageData = { + pageId: contentPageData.system.uid, + lang: contentPageData.system.locale as Lang, + publishedDate: contentPageData.system.updated_at, + createdDate: contentPageData.system.created_at, + channel: TrackingChannelEnum["static-content-page"], + pageType: "staticcontentpage", + } + + return { + contentPage, + tracking, + } + }), +}) diff --git a/server/routers/contentstack/index.ts b/server/routers/contentstack/index.ts index be1ef1ed3..c04dc867d 100644 --- a/server/routers/contentstack/index.ts +++ b/server/routers/contentstack/index.ts @@ -3,9 +3,11 @@ import { router } from "@/server/trpc" import { accountPageRouter } from "./accountPage" import { baseRouter } from "./base" import { breadcrumbsRouter } from "./breadcrumbs" +import { contentPageRouter } from "./contentPage" import { hotelPageRouter } from "./hotelPage" import { languageSwitcherRouter } from "./languageSwitcher" import { loyaltyPageRouter } from "./loyaltyPage" +import { metaDataRouter } from "./metadata" import { myPagesRouter } from "./myPages" export const contentstackRouter = router({ @@ -15,5 +17,7 @@ export const contentstackRouter = router({ hotelPage: hotelPageRouter, languageSwitcher: languageSwitcherRouter, loyaltyPage: loyaltyPageRouter, + contentPage: contentPageRouter, myPages: myPagesRouter, + metaData: metaDataRouter, }) diff --git a/server/routers/contentstack/languageSwitcher/query.ts b/server/routers/contentstack/languageSwitcher/query.ts index a41fd2a99..35b8a3ded 100644 --- a/server/routers/contentstack/languageSwitcher/query.ts +++ b/server/routers/contentstack/languageSwitcher/query.ts @@ -5,6 +5,10 @@ import { GetDaDeEnUrlsAccountPage, GetFiNoSvUrlsAccountPage, } from "@/lib/graphql/Query/AccountPage.graphql" +import { + GetDaDeEnUrlsContentPage, + GetFiNoSvUrlsContentPage, +} from "@/lib/graphql/Query/ContentPage.graphql" import { GetDaDeEnUrlsHotelPage, GetFiNoSvUrlsHotelPage, @@ -101,6 +105,20 @@ async function getLanguageSwitcher(options: LanguageSwitcherVariables) { tags: tagsFiNoSv, }, ]) + case PageTypeEnum.contentPage: + return await batchRequest([ + { + document: GetDaDeEnUrlsContentPage, + variables, + tags: tagsDaDeEn, + }, + { + document: GetFiNoSvUrlsContentPage, + variables, + tags: tagsFiNoSv, + }, + ]) + default: console.error(`type: [${options.contentType}]`) console.error(`Trying to get a content type that is not supported`) diff --git a/server/routers/contentstack/loyaltyPage/output.ts b/server/routers/contentstack/loyaltyPage/output.ts index 205509a4c..fb6d5e6cc 100644 --- a/server/routers/contentstack/loyaltyPage/output.ts +++ b/server/routers/contentstack/loyaltyPage/output.ts @@ -2,7 +2,9 @@ import { z } from "zod" import { Lang } from "@/constants/languages" -import { ImageVaultAsset } from "@/types/components/imageVaultImage" +import { imageVaultAssetSchema } from "../schemas/imageVault" + +import { ImageVaultAsset } from "@/types/components/imageVault" import { JoinLoyaltyContactTypenameEnum, LoyaltyBlocksTypenameEnum, @@ -191,6 +193,8 @@ const loyaltyPageSidebarItem = z.discriminatedUnion("__typename", [ export const validateLoyaltyPageSchema = z.object({ heading: z.string().nullable(), + preamble: z.string().nullable(), + hero_image: imageVaultAssetSchema.nullable().optional(), blocks: z.array(loyaltyPageBlockItem).nullable(), sidebar: z.array(loyaltyPageSidebarItem).nullable(), system: z.object({ @@ -258,7 +262,11 @@ export type Sidebar = | SideBarDynamicContent type LoyaltyPageDataRaw = z.infer -export type LoyaltyPage = Omit & { +export type LoyaltyPage = Omit< + LoyaltyPageDataRaw, + "blocks" | "sidebar" | "hero_image" +> & { + heroImage?: ImageVaultAsset blocks: Block[] sidebar: Sidebar[] } diff --git a/server/routers/contentstack/loyaltyPage/query.ts b/server/routers/contentstack/loyaltyPage/query.ts index c23422c4a..34bc7aa24 100644 --- a/server/routers/contentstack/loyaltyPage/query.ts +++ b/server/routers/contentstack/loyaltyPage/query.ts @@ -12,7 +12,7 @@ import { generateTag, generateTags, } from "@/utils/generateTag" -import { insertResponseToImageVaultAsset } from "@/utils/imageVault" +import { makeImageVaultImage } from "@/utils/imageVault" import { removeMultipleSlashes } from "@/utils/url" import { removeEmptyObjects } from "../../utils" @@ -22,9 +22,8 @@ import { validateLoyaltyPageRefsSchema, validateLoyaltyPageSchema, } from "./output" -import { getConnections } from "./utils" +import { getConnections, makeButtonObject } from "./utils" -import { InsertResponse } from "@/types/components/imageVaultImage" import { LoyaltyBlocksTypenameEnum, LoyaltyCardsGridEnum, @@ -35,35 +34,6 @@ import { TrackingSDKPageData, } from "@/types/components/tracking" -function makeImageVaultImage(image: any) { - return image && !!Object.keys(image).length - ? insertResponseToImageVaultAsset(image as InsertResponse) - : undefined -} - -function makeButtonObject(button: any) { - if (!button) return null - - const isContenstackLink = - button?.is_contentstack_link || button.linkConnection?.edges?.length - - return { - openInNewTab: button?.open_in_new_tab, - title: - button.cta_text || - (isContenstackLink - ? button.linkConnection.edges[0].node.title - : button.external_link.title), - href: isContenstackLink - ? button.linkConnection.edges[0].node.web?.original_url || - removeMultipleSlashes( - `/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}` - ) - : button.external_link.href, - isExternal: !isContenstackLink, - } -} - export const loyaltyPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { const { lang, uid } = ctx @@ -210,6 +180,8 @@ export const loyaltyPageQueryRouter = router({ const loyaltyPage = { heading: response.data.loyalty_page.heading, + preamble: response.data.loyalty_page.preamble, + heroImage: makeImageVaultImage(response.data.loyalty_page.hero_image), system: response.data.loyalty_page.system, blocks, sidebar, @@ -217,7 +189,6 @@ export const loyaltyPageQueryRouter = router({ const validatedLoyaltyPage = validateLoyaltyPageSchema.safeParse(loyaltyPage) - if (!validatedLoyaltyPage.success) { console.error( `Failed to validate Loyaltypage Data - (lang: ${lang}, uid: ${uid})` @@ -237,7 +208,7 @@ export const loyaltyPageQueryRouter = router({ // Assert LoyaltyPage type to get correct typings for RTE fields return { - loyaltyPage: validatedLoyaltyPage.data as LoyaltyPage, + loyaltyPage, tracking: loyaltyTrackingData, } }), diff --git a/server/routers/contentstack/loyaltyPage/utils.ts b/server/routers/contentstack/loyaltyPage/utils.ts index 95e251075..80e1f4208 100644 --- a/server/routers/contentstack/loyaltyPage/utils.ts +++ b/server/routers/contentstack/loyaltyPage/utils.ts @@ -1,3 +1,5 @@ +import { removeMultipleSlashes } from "@/utils/url" + import { LoyaltyPageRefsDataRaw } from "./output" import { @@ -77,3 +79,28 @@ export function getConnections(refs: LoyaltyPageRefsDataRaw) { return connections } +export function makeButtonObject(button: any) { + if (!button) return null + + const isContenstackLink = + button?.is_contentstack_link || button.linkConnection?.edges?.length + const linkConnnectionNode = isContenstackLink + ? button.linkConnection.edges[0]?.node + : null + + return { + openInNewTab: button?.open_in_new_tab, + title: + button.cta_text || + (linkConnnectionNode + ? linkConnnectionNode.title + : button.external_link.title), + href: linkConnnectionNode + ? linkConnnectionNode.web?.original_url || + removeMultipleSlashes( + `/${linkConnnectionNode.system.locale}/${linkConnnectionNode.url}` + ) + : button.external_link.href, + isExternal: !isContenstackLink, + } +} diff --git a/server/routers/contentstack/metadata/index.ts b/server/routers/contentstack/metadata/index.ts new file mode 100644 index 000000000..fa2618123 --- /dev/null +++ b/server/routers/contentstack/metadata/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { metaDataQueryRouter } from "./query" + +export const metaDataRouter = mergeRouters(metaDataQueryRouter) diff --git a/server/routers/contentstack/metadata/output.ts b/server/routers/contentstack/metadata/output.ts new file mode 100644 index 000000000..12024b3c6 --- /dev/null +++ b/server/routers/contentstack/metadata/output.ts @@ -0,0 +1,66 @@ +import { z } from "zod" + +export const getMetaDataSchema = z.object({ + breadcrumbsTitle: z.string().optional(), + title: z.string().optional(), + description: z.string().optional(), + imageConnection: z + .object({ + edges: z.array( + z.object({ + node: z.object({ + url: z.string(), + }), + }) + ), + }) + .optional(), +}) + +const page = z.object({ + web: z.object({ + seo_metadata: z.object({ + title: z.string().optional(), + description: z.string().optional(), + imageConnection: z + .object({ + edges: z.array( + z.object({ + node: z.object({ + url: z.string(), + }), + }) + ), + }) + .optional(), + }), + breadcrumbs: z.object({ + title: z.string(), + }), + }), + system: z.object({ + uid: z.string(), + }), +}) + +export type Page = z.infer + +const metaDataItems = z.object({ + items: z.array(page), +}) + +export const validateMyPagesMetaDataContentstackSchema = z.object({ + all_account_page: metaDataItems, +}) + +export type GetMyPagesMetaDataData = z.infer< + typeof validateMyPagesMetaDataContentstackSchema +> + +export const validateLoyaltyPageMetaDataContentstackSchema = z.object({ + all_loyalty_page: metaDataItems, +}) + +export type GetLoyaltyPageMetaDataData = z.infer< + typeof validateLoyaltyPageMetaDataContentstackSchema +> diff --git a/server/routers/contentstack/metadata/query.ts b/server/routers/contentstack/metadata/query.ts new file mode 100644 index 000000000..38d773248 --- /dev/null +++ b/server/routers/contentstack/metadata/query.ts @@ -0,0 +1,71 @@ +import { GetLoyaltyPageMetaData } from "@/lib/graphql/Query/MetaDataLoyaltyPage.graphql" +import { GetMyPagesMetaData } from "@/lib/graphql/Query/MetaDataMyPages.graphql" +import { contentstackExtendedProcedureUID, router } from "@/server/trpc" + +import { + type GetLoyaltyPageMetaDataData, + type GetMyPagesMetaDataData, + validateLoyaltyPageMetaDataContentstackSchema, + validateMyPagesMetaDataContentstackSchema, +} from "./output" +import { getMetaData, getResponse, Variables } from "./utils" + +import { PageTypeEnum } from "@/types/requests/pageType" + +async function getLoyaltyPageMetaData(variables: Variables) { + const response = await getResponse( + GetLoyaltyPageMetaData, + variables + ) + + const validatedMetaDataData = + validateLoyaltyPageMetaDataContentstackSchema.safeParse(response.data) + + if (!validatedMetaDataData.success) { + console.error( + `Failed to validate Loyaltypage MetaData Data - (url: ${variables.url})` + ) + console.error(validatedMetaDataData.error) + return null + } + + return getMetaData(validatedMetaDataData.data.all_loyalty_page.items[0]) +} + +async function getMyPagesMetaData(variables: Variables) { + const response = await getResponse( + GetMyPagesMetaData, + variables + ) + + const validatedMetaDataData = + validateMyPagesMetaDataContentstackSchema.safeParse(response.data) + + if (!validatedMetaDataData.success) { + console.error( + `Failed to validate My Page MetaData Data - (url: ${variables.url})` + ) + console.error(validatedMetaDataData.error) + return null + } + + return getMetaData(validatedMetaDataData.data.all_account_page.items[0]) +} + +export const metaDataQueryRouter = router({ + get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { + const variables = { + locale: ctx.lang, + url: ctx.pathname, + } + + switch (ctx.contentType) { + case PageTypeEnum.accountPage: + return await getMyPagesMetaData(variables) + case PageTypeEnum.loyaltyPage: + return await getLoyaltyPageMetaData(variables) + default: + return [] + } + }), +}) diff --git a/server/routers/contentstack/metadata/utils.ts b/server/routers/contentstack/metadata/utils.ts new file mode 100644 index 000000000..661c2b89d --- /dev/null +++ b/server/routers/contentstack/metadata/utils.ts @@ -0,0 +1,35 @@ +import { Lang } from "@/constants/languages" +import { request } from "@/lib/graphql/request" +import { internalServerError, notFound } from "@/server/errors/trpc" + +import { getMetaDataSchema, Page } from "./output" + +export type Variables = { + locale: Lang + url: string +} + +export async function getResponse(query: string, variables: Variables) { + const response = await request(query, variables) + if (!response.data) { + throw notFound(response) + } + + return response +} + +export function getMetaData(page: Page) { + const pageMetaData = { + breadcrumbsTitle: page.web.breadcrumbs.title, + title: page.web.seo_metadata.title, + description: page.web.seo_metadata.description, + imageConnection: page.web.seo_metadata.imageConnection, + uid: page.system.uid, + } + const validatedMetaData = getMetaDataSchema.safeParse(pageMetaData) + if (!validatedMetaData.success) { + throw internalServerError(validatedMetaData.error) + } + + return validatedMetaData.data +} diff --git a/server/routers/contentstack/schemas/imageVault.ts b/server/routers/contentstack/schemas/imageVault.ts new file mode 100644 index 000000000..87d76c9b0 --- /dev/null +++ b/server/routers/contentstack/schemas/imageVault.ts @@ -0,0 +1,95 @@ +import { z } from "zod" + +const metaData = z.object({ + DefinitionType: z.number().nullable().optional(), + Description: z.string().nullable(), + LanguageId: z.number().nullable(), + MetadataDefinitionId: z.number(), + Name: z.string(), + Value: z.string().nullable(), +}) + +/** + * Defines a media asset, original or conversion + */ +const mediaConversion = z.object({ + /** + * Aspect ratio of the conversion + */ + AspectRatio: z.number(), + /** + * Content type of the conversion + */ + ContentType: z.string(), + /** + * Aspect ratio of the selected/requested format + */ + FormatAspectRatio: z.number(), + /** + * Height of the selected/requested format + */ + FormatHeight: z.number(), + /** + * Width of the selected/requested format + */ + FormatWidth: z.number(), + /** + * Height, in pixels, of the conversion + */ + Height: z.number(), + /** + * Html representing the conversion + */ + Html: z.string(), + /** + * Id of the selected media format + */ + MediaFormatId: z.number(), + /** + * Name of the media format + */ + MediaFormatName: z.string(), + /** + * Name of the conversion + */ + Name: z.string(), + /** + * The url to the conversion + */ + Url: z.string(), + /** + * Width, in pixels, of the conversion + */ + Width: z.number(), +}) + +/** + * The response from ImageVault when inserting an asset + */ +export const imageVaultAssetSchema = z.object({ + /** + * The media item id of the asset + */ + Id: z.number(), + /** + * The id of the vault where the asset resides + */ + VaultId: z.number(), + /** + * The name of the asset + */ + Name: z.string(), + /** + * The conversion selected by the user. Is an array but will only contain one object + */ + MediaConversions: z.array(mediaConversion), + Metadata: z.array(metaData), + /** + * Date when the asset was added to ImageVault + */ + DateAdded: z.string(), + /** + * Name of the user that added the asset to ImageVault + */ + AddedBy: z.string(), +}) diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 6e21035fe..21941006d 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -1,5 +1,6 @@ import { mergeRouters } from "@/server/trpc" +import { userMutationRouter } from "./mutation" import { userQueryRouter } from "./query" -export const userRouter = mergeRouters(userQueryRouter) +export const userRouter = mergeRouters(userQueryRouter, userMutationRouter) diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index 822b1d46d..a3dea492e 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -1,5 +1,7 @@ import { z } from "zod" +import { Lang } from "@/constants/languages" + export const getUserInputSchema = z .object({ mask: z.boolean().default(true), @@ -19,13 +21,15 @@ export const soonestUpcomingStaysInput = z }) .default({ limit: 3 }) -export const initiateSaveCardInput = z.object({ +export const addCreditCardInput = z.object({ language: z.string(), - mobileToken: z.boolean(), - redirectUrl: z.string(), }) -export const saveCardInput = z.object({ +export const deleteCreditCardInput = z.object({ + creditCardId: z.string(), +}) + +export const saveCreditCardInput = z.object({ transactionId: z.string(), merchantId: z.string().optional(), }) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts new file mode 100644 index 000000000..cf568725c --- /dev/null +++ b/server/routers/user/mutation.ts @@ -0,0 +1,85 @@ +import * as api from "@/lib/api" +import { initiateSaveCardSchema } from "@/server/routers/user/output" +import { protectedProcedure, router } from "@/server/trpc" + +import { + addCreditCardInput, + deleteCreditCardInput, + saveCreditCardInput, +} from "./input" + +export const userMutationRouter = router({ + creditCard: router({ + add: protectedProcedure.input(addCreditCardInput).mutation(async function ({ + ctx, + input, + }) { + const apiResponse = await api.post(api.endpoints.v1.intiateSaveCard, { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + body: { + language: input.language, + mobileToken: false, + redirectUrl: `api/web/add-card-callback/${input.language}`, + }, + }) + + if (!apiResponse.ok) { + console.info(`API Response Failed - Initiating add Creadit Card flow`) + console.error(apiResponse) + return null + } + + const apiJson = await apiResponse.json() + const verifiedData = initiateSaveCardSchema.safeParse(apiJson) + if (!verifiedData.success) { + console.error(`Failed to initiate save card data`) + console.error(verifiedData.error) + return null + } + + return verifiedData.data.data + }), + save: protectedProcedure + .input(saveCreditCardInput) + .mutation(async function ({ ctx, input }) { + const apiResponse = await api.post( + `${api.endpoints.v1.creditCards}/${input.transactionId}`, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + } + ) + + if (!apiResponse.ok) { + console.error(`API Response Failed - Save card`) + console.error(apiResponse) + return false + } + + return true + }), + delete: protectedProcedure + .input(deleteCreditCardInput) + .mutation(async function ({ ctx, input }) { + const apiResponse = await api.remove( + `${api.endpoints.v1.creditCards}/${input.creditCardId}`, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + } + ) + + if (!apiResponse.ok) { + console.error(`API Response Failed - Delete credit card`) + console.error(apiResponse) + return false + } + + return true + }), + }), +}) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index 77f8e992a..e135ace77 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -115,6 +115,8 @@ export const getFriendTransactionsSchema = z.object({ hotelOperaId: z.string().default(""), nights: z.number().default(1), pointsCalculated: z.boolean().default(true), + transactionDate: z.string().default(""), + bookingUrl: z.string().default(""), hotelInformation: z .object({ city: z.string().default(""), @@ -170,20 +172,32 @@ export const getFriendTransactionsSchema = z.object({ .nullable(), }) -export const getCreditCardsSchema = z.object({ - data: z.array( - z.object({ - attribute: z.object({ - cardName: z.string().optional(), - alias: z.string(), - truncatedNumber: z.string(), - expirationDate: z.string(), - cardType: z.string(), - }), - id: z.string(), - type: z.string(), - }) - ), +type GetFriendTransactionsData = z.infer + +export type FriendTransaction = GetFriendTransactionsData["data"][number] + +export const creditCardSchema = z + .object({ + attribute: z.object({ + cardName: z.string().optional(), + alias: z.string(), + truncatedNumber: z.string(), + expirationDate: z.string(), + cardType: z.string(), + }), + id: z.string(), + type: z.string(), + }) + .transform((apiResponse) => { + return { + id: apiResponse.id, + type: apiResponse.attribute.cardType, + truncatedNumber: apiResponse.attribute.truncatedNumber, + } + }) + +export const creditCardsSchema = z.object({ + data: z.array(creditCardSchema), }) export const getMembershipCardsSchema = z.array( diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 7f04257de..2f3f1c5c6 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,12 +1,6 @@ import { Lang } from "@/constants/languages" import { env } from "@/env/server" import * as api from "@/lib/api" -import { internalServerError } from "@/server/errors/next" -import { - badRequestError, - forbiddenError, - unauthorizedError, -} from "@/server/errors/trpc" import { protectedProcedure, router, @@ -21,23 +15,22 @@ import encryptValue from "../utils/encryptValue" import { friendTransactionsInput, getUserInputSchema, - initiateSaveCardInput, - saveCardInput, staysInput, } from "./input" import { - getCreditCardsSchema, + creditCardsSchema, + FriendTransaction, getFriendTransactionsSchema, getMembershipCardsSchema, getStaysSchema, getUserSchema, - initiateSaveCardSchema, Stay, } from "./output" import { benefits, extendedUser, nextLevelPerks } from "./temp" import type { Session } from "next-auth" +import { RewardTransactionTypes } from "@/types/components/myPages/myPage/enums" import type { LoginType, TrackingSDKUserData, @@ -62,8 +55,14 @@ async function getVerifiedUser({ session }: { session: Session }) { return { error: true, cause: "unauthorized" } as const } else if (apiResponse.status === 403) { return { error: true, cause: "forbidden" } as const + } else if (apiResponse.status === 404) { + return { error: true, cause: "notfound" } as const } - return null + return { + error: true, + cause: "unknown", + status: apiResponse.status, + } as const } const apiJson = await apiResponse.json() @@ -91,11 +90,23 @@ function fakingRequest(payload: T): Promise { }) } -const updateStaysBookingUrl = async ( +async function updateStaysBookingUrl( data: Stay[], token: string, lang: Lang -) => { +): Promise + +async function updateStaysBookingUrl( + data: FriendTransaction[], + token: string, + lang: Lang +): Promise + +async function updateStaysBookingUrl( + data: Stay[] | FriendTransaction[], + token: string, + lang: Lang +) { // Tenporary API call needed till we have user name in ctx session data const apiResponse = await api.get(api.endpoints.v1.profile, { cache: "no-store", @@ -135,9 +146,9 @@ const updateStaysBookingUrl = async ( if (apiResponse.ok) { const apiJson = await apiResponse.json() if (apiJson.data?.attributes) { - return data.map((stay: Stay) => { + return data.map((d) => { const originalString = - stay.attributes.confirmationNumber.toString() + + d.attributes.confirmationNumber.toString() + "," + apiJson.data.attributes.lastName const encryptedBookingValue = encryptValue(originalString) @@ -147,11 +158,11 @@ const updateStaysBookingUrl = async ( "?lastName=" + apiJson.data.attributes.lastName + "&bookingId=" + - stay.attributes.confirmationNumber + d.attributes.confirmationNumber return { - ...stay, + ...d, attributes: { - ...stay.attributes, + ...d.attributes, bookingUrl: bookingUrl, }, } @@ -492,15 +503,29 @@ export const userQueryRouter = router({ return null } - const pageData = verifiedData.data.data.slice( - limit * (page - 1), - limit * page + const updatedData = await updateStaysBookingUrl( + verifiedData.data.data, + ctx.session.token.access_token, + ctx.lang ) + const pageData = updatedData + .filter((t) => t.type !== RewardTransactionTypes.expired) + .sort((a, b) => { + // 'BALFWD' are transactions from Opera migration that happended in May 2021 + const isBalfwd = + a.type === RewardTransactionTypes.stayAdj && + a.attributes.confirmationNumber === "BALFWD" + if (isBalfwd) return 1 + return a.attributes.checkinDate > b.attributes.checkinDate ? -1 : 1 + }) + .slice(limit * (page - 1), limit * page) + return { data: { - transactions: pageData.map(({ attributes }) => { + transactions: pageData.map(({ type, attributes }) => { return { + type, awardPoints: attributes.awardPoints, checkinDate: attributes.checkinDate, checkoutDate: attributes.checkoutDate, @@ -508,6 +533,10 @@ export const userQueryRouter = router({ confirmationNumber: attributes.confirmationNumber, hotelName: attributes.hotelInformation?.name, nights: attributes.nights, + pointsCalculated: attributes.pointsCalculated, + hotelId: attributes.hotelOperaId, + transactionDate: attributes.transactionDate, + bookingUrl: attributes.bookingUrl, } }), }, @@ -534,7 +563,7 @@ export const userQueryRouter = router({ } const apiJson = await apiResponse.json() - const verifiedData = getCreditCardsSchema.safeParse(apiJson) + const verifiedData = creditCardsSchema.safeParse(apiJson) if (!verifiedData.success) { console.error(`Failed to validate Credit Cards Data`) console.error(`User: (${JSON.stringify(ctx.session.user)})`) @@ -545,69 +574,6 @@ export const userQueryRouter = router({ return verifiedData.data.data }), - initiateSaveCard: protectedProcedure - .input(initiateSaveCardInput) - .mutation(async function ({ ctx, input }) { - const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: { - language: input.language, - mobileToken: input.mobileToken, - redirectUrl: input.redirectUrl, - }, - }) - - if (!apiResponse.ok) { - switch (apiResponse.status) { - case 400: - throw badRequestError(apiResponse) - case 401: - throw unauthorizedError(apiResponse) - case 403: - throw forbiddenError(apiResponse) - default: - throw internalServerError(apiResponse) - } - } - - const apiJson = await apiResponse.json() - const verifiedData = initiateSaveCardSchema.safeParse(apiJson) - if (!verifiedData.success) { - console.error(`Failed to initiate save card data`) - console.error(`User: (${JSON.stringify(ctx.session.user)})`) - console.error(verifiedData.error) - return null - } - - return verifiedData.data.data - }), - - saveCard: protectedProcedure.input(saveCardInput).mutation(async function ({ - ctx, - input, - }) { - const apiResponse = await api.post( - `${api.endpoints.v1.creditCards}/${input.transactionId}`, - { - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - body: {}, - } - ) - - if (!apiResponse.ok) { - console.error(`API Response Failed - Save card`) - console.error(`User: (${JSON.stringify(ctx.session.user)})`) - console.error(apiResponse) - return null - } - - return true - }), - membershipCards: protectedProcedure.query(async function ({ ctx }) { const apiResponse = await api.get(api.endpoints.v1.profile, { cache: "no-store", diff --git a/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts b/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts new file mode 100644 index 000000000..8d81b458d --- /dev/null +++ b/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts @@ -0,0 +1,40 @@ +export type BookingConfirmation = { + email: string + hotel: { + name: string + address: string + location: string + phone: string + image: string + checkIn: string + checkOut: string + breakfast: { + start: string + end: string + } + } + stay: { + nights: number + start: string + end: string + } + summary: { + roomType: string + bedType: string + breakfast: string + flexibility: string + } +} + +export type IntroSectionProps = { + email: BookingConfirmation["email"] +} + +export type StaySectionProps = { + hotel: BookingConfirmation["hotel"] + stay: BookingConfirmation["stay"] +} + +export type SummarySectionProps = { + summary: BookingConfirmation["summary"] +} diff --git a/types/components/imageContainer.ts b/types/components/imageContainer.ts index 6f8e75393..bd4ea2ab1 100644 --- a/types/components/imageContainer.ts +++ b/types/components/imageContainer.ts @@ -1,4 +1,4 @@ -import type { ImageVaultAsset } from "./imageVaultImage" +import type { ImageVaultAsset } from "./imageVault" export type ImageContainerProps = { leftImage: ImageVaultAsset diff --git a/types/components/imageVault.ts b/types/components/imageVault.ts new file mode 100644 index 000000000..ceae6e278 --- /dev/null +++ b/types/components/imageVault.ts @@ -0,0 +1,26 @@ +/** + * @file TypeScript typings for ImageVault + * + * The types in this file are based on the source maps of the downloaded + * distribution at https://clientscript.imagevault.se/Installation/ImageVaultInsertMedia + * + * They have been clean up and amended to. + */ + +import { z } from "zod" + +import { imageVaultAssetSchema } from "@/server/routers/contentstack/schemas/imageVault" + +export type ImageVaultAssetResponse = z.infer + +export type ImageVaultAsset = { + id: number + title: string + url: string + dimensions: { + width: number + height: number + aspectRatio: number + } + meta: { alt: string | undefined | null; caption: string | undefined | null } +} diff --git a/types/components/imageVaultImage.ts b/types/components/imageVaultImage.ts deleted file mode 100644 index c543d3243..000000000 --- a/types/components/imageVaultImage.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @file TypeScript typings for ImageVault - * - * The types in this file are based on the source maps of the downloaded - * distribution at https://clientscript.imagevault.se/Installation/ImageVaultInsertMedia - * - * They have been clean up and amended to. - */ - -export type MetaData = { - DefinitionType?: number - Description: string | null - LanguageId: null - MetadataDefinitionId: number - Name: string - Value: string -} - -export type ImageVaultAsset = { - id: number - title: string - url: string - dimensions: { - width: number - height: number - aspectRatio: number - } - meta: { alt: string | undefined; caption: string | undefined } -} - -/** - * The response from ImageVault when inserting an asset - */ -export declare class InsertResponse { - /** - * The media item id of the asset - */ - Id: number - /** - * The id of the vault where the asset resides - */ - VaultId: number - /** - * The name of the asset - */ - Name: string - /** - * The conversion selected by the user. Is an array but will only contain one object - */ - MediaConversions: MediaConversion[] - /** - * Date when the asset was added to ImageVault - */ - DateAdded: string - /** - * Name of the user that added the asset to ImageVault - */ - AddedBy: string - - Metadata?: MetaData[] | undefined -} - -/** - * Defines a media asset, original or conversion - */ -export declare class MediaConversion { - /** - * The url to the conversion - */ - Url: string - /** - * Name of the conversion - */ - Name: string - /** - * Html representing the conversion - */ - Html: string - /** - * Content type of the conversion - */ - ContentType: string - /** - * Width, in pixels, of the conversion - */ - Width: number - /** - * Height, in pixels, of the conversion - */ - Height: number - /** - * Aspect ratio of the conversion - */ - AspectRatio: number - /** - * Width of the selected/requested format - */ - FormatWidth: number - /** - * Height of the selected/requested format - */ - FormatHeight: number - /** - * Aspect ratio of the selected/requested format - */ - FormatAspectRatio: number - /** - * Name of the media format - */ - MediaFormatName: string - /** - * Id of the selected media format - */ - MediaFormatId: number -} diff --git a/types/components/metadata/index.ts b/types/components/metadata/index.ts new file mode 100644 index 000000000..bcc71f13f --- /dev/null +++ b/types/components/metadata/index.ts @@ -0,0 +1,5 @@ +import { z } from "zod" + +import { getMetaDataSchema } from "@/server/routers/contentstack/metadata/output" + +export interface MetaData extends z.infer {} diff --git a/types/components/myPages/myPage/earnAndBurn.ts b/types/components/myPages/myPage/earnAndBurn.ts index ebe3060ff..9bc0c69aa 100644 --- a/types/components/myPages/myPage/earnAndBurn.ts +++ b/types/components/myPages/myPage/earnAndBurn.ts @@ -1,4 +1,4 @@ -import { awardPointsVariants } from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/awardPointsVariants" +import { awardPointsVariants } from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants" import type { VariantProps } from "class-variance-authority" diff --git a/types/components/myPages/myPage/enums.ts b/types/components/myPages/myPage/enums.ts index 294944620..67ee8e12a 100644 --- a/types/components/myPages/myPage/enums.ts +++ b/types/components/myPages/myPage/enums.ts @@ -16,3 +16,17 @@ export enum ContentEntries { AccountPageContentShortcuts = "AccountPageContentShortcuts", AccountPageContentTextContent = "AccountPageContentTextContent", } + +export enum RewardTransactionTypes { + stay = "stay", + rewardNight = "rewardnight", + enrollment = "enrollment", + expired = "expired", + redgift = "redgift", + ancillary = "ancillary", + pointShop = "pointshop", + tui_points = "tui_points", + mastercard_points = "mastercard_points", + stayAdj = "stay/adj", + othersAdj = "others/adj", +} diff --git a/types/components/myPages/myProfile/addCreditCardButton.ts b/types/components/myPages/myProfile/addCreditCardButton.ts deleted file mode 100644 index 61a8e3aef..000000000 --- a/types/components/myPages/myProfile/addCreditCardButton.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AddCreditCardButtonProps = { - redirectUrl: string -} diff --git a/types/components/myPages/myProfile/card.ts b/types/components/myPages/myProfile/card.ts deleted file mode 100644 index cece9fef6..000000000 --- a/types/components/myPages/myProfile/card.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface CardProps extends React.HTMLAttributes { - tag?: "article" | "div" | "section" -} diff --git a/types/components/myPages/myProfile/creditCards.ts b/types/components/myPages/myProfile/creditCards.ts new file mode 100644 index 000000000..6290f5288 --- /dev/null +++ b/types/components/myPages/myProfile/creditCards.ts @@ -0,0 +1,9 @@ +import type { CreditCard } from "@/types/user" + +export type CreditCardRowProps = { + card: CreditCard +} + +export type DeleteCreditCardConfirmationProps = { + card: CreditCard +} diff --git a/types/components/tracking.ts b/types/components/tracking.ts index 354212d8d..92128d939 100644 --- a/types/components/tracking.ts +++ b/types/components/tracking.ts @@ -4,6 +4,7 @@ import type { Lang } from "@/constants/languages" export enum TrackingChannelEnum { "scandic-friends" = "scandic-friends", + "static-content-page" = "static-content-page", } export type TrackingChannel = keyof typeof TrackingChannelEnum diff --git a/types/fetch.ts b/types/fetch.ts index 5f71dc355..a727709a9 100644 --- a/types/fetch.ts +++ b/types/fetch.ts @@ -1,7 +1,7 @@ export interface RequestOptionsWithJSONBody extends Omit { - body: Record + body?: Record } export interface RequestOptionsWithOutBody - extends Omit { } + extends Omit {} diff --git a/types/requests/imageContainer.ts b/types/requests/imageContainer.ts index 3eb85023c..2a604b948 100644 --- a/types/requests/imageContainer.ts +++ b/types/requests/imageContainer.ts @@ -1,11 +1,11 @@ -import { InsertResponse } from "../components/imageVaultImage" +import { ImageVaultAssetResponse } from "../components/imageVault" import { EmbedEnum } from "./utils/embeds" import { Typename } from "./utils/typename" export type ImageContainer = Typename< { - image_left: InsertResponse - image_right: InsertResponse + image_left: ImageVaultAssetResponse + image_right: ImageVaultAssetResponse system: { uid: string } diff --git a/types/requests/pageType.ts b/types/requests/pageType.ts index 993a697e4..001e6a486 100644 --- a/types/requests/pageType.ts +++ b/types/requests/pageType.ts @@ -2,5 +2,6 @@ export enum PageTypeEnum { accountPage = "account-page", loyaltyPage = "loyalty-page", hotelPage = "hotel-page", + contentPage = "content-page", currentBlocksPage = "current-blocks-page", } diff --git a/types/rte/attrs.ts b/types/rte/attrs.ts index dd658872d..b99cca8b0 100644 --- a/types/rte/attrs.ts +++ b/types/rte/attrs.ts @@ -1,4 +1,4 @@ -import { InsertResponse } from "../components/imageVaultImage" +import { ImageVaultAssetResponse } from "../components/imageVault" import { RTEItemTypeEnum } from "./enums" import type { Lang } from "@/constants/languages" @@ -39,7 +39,9 @@ export interface RTELinkAttrs extends Attributes { type: RTEItemTypeEnum.entry } -export interface RTEImageVaultAttrs extends Attributes, InsertResponse { +export interface RTEImageVaultAttrs + extends Attributes, + ImageVaultAssetResponse { height: string width: string style: string[] diff --git a/types/trpc/routers/contentstack/contentPage.ts b/types/trpc/routers/contentstack/contentPage.ts new file mode 100644 index 000000000..d5a8cc322 --- /dev/null +++ b/types/trpc/routers/contentstack/contentPage.ts @@ -0,0 +1,13 @@ +import { z } from "zod" + +import { validateContentPageSchema } from "@/server/routers/contentstack/contentPage/output" + +import { ImageVaultAsset } from "@/types/components/imageVault" + +export type ContentPageDataRaw = z.infer + +type ContentPageRaw = ContentPageDataRaw["content_page"] + +export type ContentPage = Omit & { + heroImage?: ImageVaultAsset +} diff --git a/types/user.ts b/types/user.ts index c4599e579..a90602661 100644 --- a/types/user.ts +++ b/types/user.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { getUserSchema } from "@/server/routers/user/output" +import { creditCardSchema, getUserSchema } from "@/server/routers/user/output" type Journey = { tag: string @@ -28,3 +28,5 @@ export interface User extends z.infer { shortcuts: ShortcutLink[] victories: Victory[] } + +export type CreditCard = z.output diff --git a/utils/generateMetadata.ts b/utils/generateMetadata.ts new file mode 100644 index 000000000..547c5ccd6 --- /dev/null +++ b/utils/generateMetadata.ts @@ -0,0 +1,35 @@ +import { Metadata } from "next" + +import { serverClient } from "@/lib/trpc/server" + +import { MetaData } from "@/types/components/metadata" + +export async function generateMetadata(): Promise { + const metaData: MetaData | never[] | null = + await serverClient().contentstack.metaData.get() + + if (Array.isArray(metaData)) { + return { + title: "", + description: "", + openGraph: { + images: [], + }, + } + } + + const title = metaData?.breadcrumbsTitle ?? metaData?.title ?? "" + const description = metaData?.description ?? "" + const images = + metaData?.imageConnection?.edges?.map((edge) => ({ + url: edge.node.url, + })) || [] + + return { + title, + description, + openGraph: { + images, + }, + } +} diff --git a/utils/imageVault.ts b/utils/imageVault.ts index a6aaba6c7..d0dc7d086 100644 --- a/utils/imageVault.ts +++ b/utils/imageVault.ts @@ -1,10 +1,10 @@ import { ImageVaultAsset, - InsertResponse, -} from "@/types/components/imageVaultImage" + ImageVaultAssetResponse, +} from "@/types/components/imageVault" export function insertResponseToImageVaultAsset( - response: InsertResponse + response: ImageVaultAssetResponse ): ImageVaultAsset { const alt = response.Metadata?.find((meta) => meta.Name.includes("AltText_") @@ -29,3 +29,9 @@ export function insertResponseToImageVaultAsset( }, } } + +export function makeImageVaultImage(image: any) { + return image && !!Object.keys(image).length + ? insertResponseToImageVaultAsset(image as ImageVaultAssetResponse) + : undefined +}
{maskedCardNumber}