Merge branch 'develop' into feat/SW-185-implement-footer-navigation

This commit is contained in:
Pontus Dreij
2024-08-22 16:42:09 +02:00
139 changed files with 2834 additions and 927 deletions

View File

@@ -12,6 +12,7 @@ CURITY_CLIENT_SECRET_SERVICE="test"
CURITY_CLIENT_ID_USER="test" CURITY_CLIENT_ID_USER="test"
CURITY_CLIENT_SECRET_USER="test" CURITY_CLIENT_SECRET_USER="test"
CURITY_ISSUER_USER="test" CURITY_ISSUER_USER="test"
CURITY_ISSUER_SERVICE="test"
CYPRESS_API_BASEURL="test" CYPRESS_API_BASEURL="test"
CYPRESS_CURITY_USERNAME="test" CYPRESS_CURITY_USERNAME="test"
CYPRESS_CURITY_PASSWORD="test" CYPRESS_CURITY_PASSWORD="test"
@@ -35,3 +36,4 @@ SEAMLESS_LOGOUT_FI="test"
SEAMLESS_LOGOUT_NO="test" SEAMLESS_LOGOUT_NO="test"
SEAMLESS_LOGOUT_SV="test" SEAMLESS_LOGOUT_SV="test"
WEBVIEW_ENCRYPTION_KEY="test" WEBVIEW_ENCRYPTION_KEY="test"
BOOKING_ENCRYPTION_KEY="test"

View File

@@ -20,13 +20,17 @@ export default async function ProtectedLayout({
h.get("x-url") ?? h.get("x-pathname") ?? overview[getLang()] h.get("x-url") ?? h.get("x-pathname") ?? overview[getLang()]
) )
const redirectURL = `/${getLang()}/login?redirectTo=${redirectTo}`
if (!session) { if (!session) {
redirect(`/${getLang()}/login?redirectTo=${redirectTo}`) console.log(`[layout:protected] no session, redirecting to: ${redirectURL}`)
redirect(redirectURL)
} }
const user = await serverClient().user.get() const user = await serverClient().user.get()
if (!user || "error" in user) { if (!user || "error" in user) {
redirect(`/${getLang()}/login?redirectTo=${redirectTo}`) console.log(`[layout:protected] no user, redirecting to: ${redirectURL}`)
redirect(redirectURL)
} }
return children return children

View File

@@ -1,5 +1,3 @@
import { createActionURL } from "@auth/core"
import { headers as nextHeaders } from "next/headers"
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { AuthError } from "next-auth" import { AuthError } from "next-auth"
@@ -16,11 +14,35 @@ export async function GET(
let redirectTo: string = "" let redirectTo: string = ""
const returnUrl = request.headers.get("x-returnurl") const returnUrl = request.headers.get("x-returnurl")
const isSeamless = request.headers.get("x-logout-source") === "seamless"
console.log(
`[logout] source: ${request.headers.get("x-logout-source") || "normal"}`
)
const redirectToSearchParamValue =
request.nextUrl.searchParams.get("redirectTo")
const redirectToFallback = "/"
if (isSeamless) {
if (returnUrl) { if (returnUrl) {
// Seamless logout request from Current web
redirectTo = returnUrl redirectTo = returnUrl
} else { } 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 { try {
// Initiate the seamless logout flow // Initiate the seamless logout flow
let redirectUrlValue let redirectUrlValue
@@ -45,6 +67,9 @@ export async function GET(
break break
} }
const redirectUrl = new URL(redirectUrlValue) const redirectUrl = new URL(redirectUrlValue)
console.log(
`[logout] creating redirect to seamless logout: ${redirectUrl}`
)
redirectTo = redirectUrl.toString() redirectTo = redirectUrl.toString()
} catch (e) { } catch (e) {
console.error( console.error(
@@ -55,37 +80,25 @@ export async function GET(
} }
try { 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 * Passing `redirect: false` to `signOut` will return a result object
* instead of automatically redirecting inside of `signOut`. * instead of automatically redirecting inside of `signOut`.
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L104 * 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({ const redirectUrlObj = await signOut({
redirectTo: curityLogoutUrl, redirectTo,
redirect: false, redirect: false,
}) })
if (redirectUrlObj) { if (redirectUrlObj) {
console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`)
return NextResponse.redirect(redirectUrlObj.redirect) return NextResponse.redirect(redirectUrlObj.redirect)
} else {
console.error(`[logout] missing redirectUrlObj reponse from signOut()`)
} }
} catch (error) { } catch (error) {
if (error instanceof AuthError) { if (error instanceof AuthError) {

View File

@@ -34,7 +34,6 @@ export default async function MyPages({
<p>{formatMessage({ id: "No content published" })}</p> <p>{formatMessage({ id: "No content published" })}</p>
)} )}
</main> </main>
<TrackingSDK pageData={tracking} /> <TrackingSDK pageData={tracking} />
</> </>
) )

View File

@@ -14,17 +14,6 @@
gap: var(--Spacing-x1); 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) { @media screen and (min-width: 768px) {
.container { .container {
gap: var(--Spacing-x3); gap: var(--Spacing-x3);

View File

@@ -1,11 +1,9 @@
import { env } from "@/env/server" import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { CreditCard, Delete } from "@/components/Icons"
import AddCreditCardButton from "@/components/Profile/AddCreditCardButton" 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 Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
@@ -33,40 +31,8 @@ export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
})} })}
</Body> </Body>
</article> </article>
{creditCards?.length ? ( <CreditCardList initialData={creditCards} />
<div className={styles.cardContainer}> <AddCreditCardButton />
{creditCards.map((card, idx) => (
<CreditCardRow
key={idx}
cardType={card.attribute.cardType}
truncatedNumber={card.attribute.truncatedNumber}
/>
))}
</div>
) : null}
<AddCreditCardButton
redirectUrl={`${env.PUBLIC_URL}/api/web/add-card-callback/${lang}`}
/>
</section> </section>
) )
} }
function CreditCardRow({
truncatedNumber,
cardType,
}: {
truncatedNumber: string
cardType: string
}) {
const maskedCardNumber = `**** ${truncatedNumber.slice(12, 16)}`
return (
<div className={styles.card}>
<CreditCard color="black" />
<Body textTransform="bold">{cardType}</Body>
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
<Button variant="icon" theme="base" intent="text">
<Delete color="burgundy" />
</Button>
</div>
)
}

View File

@@ -15,7 +15,8 @@ export default function ProfileLayout({
{profile} {profile}
<Divider color="burgundy" opacity={8} /> <Divider color="burgundy" opacity={8} />
{creditCards} {creditCards}
{communication} {/* TODO: Implement communication preferences flow. Hidden until decided on where to send user. */}
{/* {communication} */}
</section> </section>
</main> </main>
) )

View File

@@ -1,8 +1,8 @@
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import ContentPage from "@/components/ContentType/ContentPage" import ContentPage from "@/components/ContentType/ContentPage"
import HotelPage from "@/components/ContentType/HotelPage/HotelPage" import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage/LoyaltyPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import { import {

View File

@@ -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;
}
}

View File

@@ -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 (
<main className={styles.main}>
<section className={styles.section}>
<IntroSection email={email} />
<StaySection hotel={hotel} stay={stay} />
<SummarySection summary={summary} />
</section>
</main>
)
}

View File

@@ -11,32 +11,51 @@ export async function GET(
request: NextRequest, request: NextRequest,
context: { params: { lang: Lang } } 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) { if (!env.PUBLIC_URL) {
throw internalServerError("No value for env.PUBLIC_URL") throw internalServerError("No value for env.PUBLIC_URL")
} }
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) { if (returnUrl) {
// Seamless login request from Current web
redirectTo = returnUrl redirectTo = returnUrl
} else { } else {
// Normal login request from New web console.log(
`[login] missing returnUrl, using fallback: ${redirectToFallback}`
)
redirectTo = redirectToFallback
}
} else {
redirectTo = redirectTo =
request.cookies.get("redirectTo")?.value || // Cookie gets set by authRequired middleware redirectToCookieValue || redirectToSearchParamValue || redirectToFallback
request.nextUrl.searchParams.get("redirectTo") ||
"/"
// Make relative URL to absolute URL // Make relative URL to absolute URL
if (redirectTo.startsWith("/")) { if (redirectTo.startsWith("/")) {
console.log(`[login] make redirectTo absolute, from ${redirectTo}`)
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
console.log(`[login] make redirectTo absolute, to ${redirectTo}`)
} }
// Clean up cookie from authRequired middleware // Clean up cookie from authRequired middleware
@@ -70,7 +89,11 @@ export async function GET(
break break
} }
const redirectUrl = new URL(redirectUrlValue) const redirectUrl = new URL(redirectUrlValue)
console.log(`[login] creating redirect to seamless login: ${redirectUrl}`)
redirectUrl.searchParams.set("returnurl", redirectTo) redirectUrl.searchParams.set("returnurl", redirectTo)
console.log(
`[login] returnurl for seamless login: ${redirectUrl.searchParams.get("returnurl")}`
)
redirectTo = redirectUrl.toString() redirectTo = redirectUrl.toString()
/** Set cookie with redirect Url to appropriately redirect user when using magic link login */ /** Set cookie with redirect Url to appropriately redirect user when using magic link login */
@@ -82,25 +105,20 @@ export async function GET(
) )
} catch (e) { } catch (e) {
console.error( 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 { try {
/** console.log(`[login] 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_NEXTAUTH_URL: process.env.NEXTAUTH_URL })
console.log({ login_env: process.env }) console.log({ login_env: process.env })
console.log({ login_redirectTo: redirectTo }) /** Record<string, any> is next-auth typings */
const params = { const params: Record<string, any> = {
ui_locales: context.params.lang, 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 * The `acr_values` param is used to make Curity display the proper login
* page for Scandic. Without the parameter Curity presents some choices * 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 // This is new param set for differentiate between the Magic link login of New web and current web
version: "2", version: "2",
} }
if (isMFA) { if (isMFA) {
// Append profile_update scope for MFA // 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 * 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 * while in current web it is being setup using different Curity Client
*/ */
params.acr_values = params.acr_values =
"urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web" "urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web"
} else if (isMagicLinkUpdateLogin) { } else if (isSeamlessMagicLink) {
params.acr_values = "abc" 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( const redirectUrl = await signIn(
"curity", "curity",
{ {
@@ -139,9 +164,13 @@ export async function GET(
) )
if (redirectUrl) { if (redirectUrl) {
return NextResponse.redirect(redirectUrl, { const redirectOpts = {
headers: redirectHeaders, 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) { } catch (error) {
if (error instanceof AuthError) { if (error instanceof AuthError) {

View File

@@ -12,39 +12,54 @@ export async function GET(
request: NextRequest, request: NextRequest,
context: { params: { lang: Lang } } 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) { if (!env.PUBLIC_URL) {
throw internalServerError("No value for 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 // Make relative URL to absolute URL
if (redirectTo.startsWith("/")) { if (redirectTo.startsWith("/")) {
console.log(
`[verifymagiclink] make redirectTo absolute, from ${redirectTo}`
)
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href 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 // Update Seamless login url as Magic link login has a different authenticator in Curity
redirectTo = redirectTo.replace("updatelogin", "updateloginemail") redirectTo = redirectTo.replace("updatelogin", "updateloginemail")
const loginKey = request.nextUrl.searchParams.get("loginKey")
if (!loginKey) {
return badRequest()
}
try { try {
console.log(`[verifymagiclink] final redirectUrl: ${redirectTo}`)
/** /**
* Passing `redirect: false` to `signIn` will return the URL instead of * Passing `redirect: false` to `signIn` will return the URL instead of
* automatically redirecting to it inside of `signIn`. * automatically redirecting to it inside of `signIn`.
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
*/ */
console.log({ login_redirectTo: redirectTo }) const redirectUrl = await signIn(
let redirectUrl = await signIn(
"curity", "curity",
{ {
redirectTo, redirectTo,
@@ -61,7 +76,12 @@ export async function GET(
) )
if (redirectUrl) { if (redirectUrl) {
console.log(`[verifymagiclink] redirecting to: ${redirectUrl}`)
return NextResponse.redirect(redirectUrl) return NextResponse.redirect(redirectUrl)
} else {
console.error(
`[verifymagiclink] missing redirectUrl reponse from signIn()`
)
} }
} catch (error) { } catch (error) {
if (error instanceof AuthError) { if (error instanceof AuthError) {

View File

@@ -15,14 +15,9 @@ import { getIntl } from "@/i18n"
import ServerIntlProvider from "@/i18n/Provider" import ServerIntlProvider from "@/i18n/Provider"
import { getLang, setLang } from "@/i18n/serverContext" import { getLang, setLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, LayoutArgs } from "@/types/params" import type { LangParams, LayoutArgs } from "@/types/params"
export const metadata: Metadata = { export { generateMetadata } from "@/utils/generateMetadata"
description: "New web",
title: "Scandic Hotels",
}
export default async function RootLayout({ export default async function RootLayout({
children, children,
@@ -35,8 +30,8 @@ export default async function RootLayout({
>) { >) {
setLang(params.lang) setLang(params.lang)
preloadUserTracking() preloadUserTracking()
const { defaultLocale, locale, messages } = await getIntl() const { defaultLocale, locale, messages } = await getIntl()
return ( return (
<html lang={getLang()}> <html lang={getLang()}>
<head> <head>

View File

@@ -1,10 +1,6 @@
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params" export default async function NotFound() {
export default async function NotFound({ params }: PageArgs<LangParams>) {
setLang(params.lang)
const { formatMessage } = await getIntl() const { formatMessage } = await getIntl()
return ( return (
<main> <main>

View File

@@ -21,6 +21,7 @@ export default async function ContentTypePage({
const user = await serverClient().user.get() const user = await serverClient().user.get()
if (!user) { if (!user) {
console.log(`[webview:page] unable to load user`)
return <p>Error: No user could be loaded</p> return <p>Error: No user could be loaded</p>
} }
@@ -31,9 +32,16 @@ export default async function ContentTypePage({
case "token_expired": case "token_expired":
const h = headers() const h = headers()
const returnURL = `/${getLang()}/webview${h.get("x-pathname")!}` const returnURL = `/${getLang()}/webview${h.get("x-pathname")!}`
redirect( const redirectURL = `/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}`
`/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` console.log(`[webview:page] user error, redirecting to: ${redirectURL}`)
) redirect(redirectURL)
case "notfound":
return <p>Error: user not found</p>
case "unknown":
return <p>Unknown error occurred loading user</p>
default:
const u: never = user
console.log(`[webview:page] unhandled user loading error`)
} }
} }

View File

@@ -2,46 +2,53 @@ import { NextRequest } from "next/server"
import { env } from "process" import { env } from "process"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
import { profile } from "@/constants/routes/myPages"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { badRequest, internalServerError } from "@/server/errors/next"
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: { lang: string } } { params }: { params: { lang: string } }
) { ) {
try { console.log(`[add-card] callback started`)
const lang = params.lang as Lang const lang = params.lang as Lang
const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`)
try {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
const success = searchParams.get("success") const success = searchParams.get("success")
const failure = searchParams.get("failure") const failure = searchParams.get("failure")
const cancel = searchParams.get("cancel")
const trxId = searchParams.get("datatransTrxId") const trxId = searchParams.get("datatransTrxId")
const returnUrl = new URL(
`${env.PUBLIC_URL}/${lang ?? Lang.en}/scandic-friends/my-pages/profile`
)
if (success) { if (success) {
if (!trxId) { if (trxId) {
return badRequest("Missing datatransTrxId param") const saveCardSuccess = await serverClient().user.creditCard.save({
}
const saveCardSuccess = await serverClient().user.saveCard({
transactionId: trxId, transactionId: trxId,
}) })
if (saveCardSuccess) { if (saveCardSuccess) {
console.log(`[add-card] planet success: card saved success`)
returnUrl.searchParams.set("success", "true") returnUrl.searchParams.set("success", "true")
} else { } else {
console.log(`[add-card] planet success: card saved fail`)
returnUrl.searchParams.set("failure", "true") returnUrl.searchParams.set("failure", "true")
} }
} else {
console.log(`[add-card] planet success: missing datatransTrxId`)
returnUrl.searchParams.set("error", "true")
}
} else if (failure) { } else if (failure) {
console.log(`[add-card] planet fail`)
returnUrl.searchParams.set("failure", "true") returnUrl.searchParams.set("failure", "true")
} else if (cancel) {
console.log(`[add-card] planet cancel`)
returnUrl.searchParams.set("cancel", "true")
}
} catch (e) {
console.error(`[add-card] error saving credit card`, e)
returnUrl.searchParams.set("error", "true")
} }
return Response.redirect(returnUrl, 307) console.log(`[add-card] redirecting to: ${returnUrl}`)
} catch (error) { return Response.redirect(returnUrl)
console.error(error)
return internalServerError()
}
} }

View File

@@ -99,6 +99,7 @@
:root { :root {
--max-width: 113.5rem; --max-width: 113.5rem;
--max-width-content: 74.75rem; --max-width-content: 74.75rem;
--max-width-text-block: 49.5rem;
} }
* { * {

View File

@@ -136,7 +136,9 @@ export const config = {
return session return session
}, },
async redirect({ baseUrl, url }) { async redirect({ baseUrl, url }) {
console.log(`[auth] deciding redirect URL`, { baseUrl, url })
if (url.startsWith("/")) { if (url.startsWith("/")) {
console.log(`[auth] relative URL accepted, returning: ${baseUrl}${url}`)
// Allows relative callback URLs // Allows relative callback URLs
return `${baseUrl}${url}` return `${baseUrl}${url}`
} else { } else {
@@ -146,17 +148,19 @@ export const config = {
if ( if (
/\.scandichotels\.(dk|de|com|fi|no|se)$/.test(parsedUrl.hostname) /\.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 // Allows any subdomains on all top level domains above
return url return url
} else if (parsedUrl.origin === baseUrl) { } else if (parsedUrl.origin === baseUrl) {
// Allows callback URLs on the same origin // Allows callback URLs on the same origin
console.log(`[auth] origin URL accepted, returning: ${url}`)
return url return url
} }
} catch (e) { } catch (e) {
console.error("Error in auth redirect callback") console.error(`[auth] error parsing incoming URL for redirection`, e)
console.error(e)
} }
} }
console.log(`[auth] URL denied, returning base URL: ${baseUrl}`)
return baseUrl return baseUrl
}, },
async authorized({ auth, request }) { async authorized({ auth, request }) {

View File

@@ -1,3 +0,0 @@
export default async function ContentPage() {
return null
}

View File

@@ -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);
}

View File

@@ -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 (
<>
<section className={styles.contentPage}>
<header className={styles.header}>
<Intro>
<Title as="h2">{contentPage.header.heading}</Title>
<Preamble>{contentPage.header.preamble}</Preamble>
</Intro>
</header>
<main className={styles.content}>
<div className={styles.innerContent}>
{heroImage ? (
<Hero
alt={heroImage.meta.alt || heroImage.meta.caption || ""}
src={heroImage.url}
/>
) : null}
</div>
</main>
</section>
<TrackingSDK pageData={tracking} />
</>
)
}

View File

@@ -1,8 +1,11 @@
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import Hero from "@/components/Hero"
import Intro from "@/components/Intro"
import { Blocks } from "@/components/Loyalty/Blocks" import { Blocks } from "@/components/Loyalty/Blocks"
import Sidebar from "@/components/Loyalty/Sidebar" import Sidebar from "@/components/Loyalty/Sidebar"
import MaxWidth from "@/components/MaxWidth" import MaxWidth from "@/components/MaxWidth"
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
@@ -16,7 +19,7 @@ export default async function LoyaltyPage() {
} }
const { tracking, loyaltyPage } = loyaltyPageRes const { tracking, loyaltyPage } = loyaltyPageRes
const heroImage = loyaltyPage.heroImage
return ( return (
<> <>
<section className={styles.content}> <section className={styles.content}>
@@ -24,8 +27,22 @@ export default async function LoyaltyPage() {
<Sidebar blocks={loyaltyPage.sidebar} /> <Sidebar blocks={loyaltyPage.sidebar} />
) : null} ) : null}
<MaxWidth className={styles.blocks} tag="main"> <MaxWidth className={styles.blocks}>
<Title>{loyaltyPage.heading}</Title> <header className={styles.header}>
<Intro>
<Title as="h2">{loyaltyPage.heading}</Title>
{loyaltyPage.preamble ? (
<Preamble>{loyaltyPage.preamble}</Preamble>
) : null}
</Intro>
{heroImage ? (
<Hero
alt={heroImage.meta.alt || heroImage.meta.caption || ""}
src={heroImage.url}
/>
) : null}
</header>
{loyaltyPage.blocks ? <Blocks blocks={loyaltyPage.blocks} /> : null} {loyaltyPage.blocks ? <Blocks blocks={loyaltyPage.blocks} /> : null}
</MaxWidth> </MaxWidth>
</section> </section>

View File

@@ -15,6 +15,11 @@
padding-right: var(--Spacing-x2); padding-right: var(--Spacing-x2);
} }
.header {
display: grid;
gap: var(--Spacing-x4);
}
@media screen and (min-width: 1367px) { @media screen and (min-width: 1367px) {
.content { .content {
gap: var(--Spacing-x5); gap: var(--Spacing-x5);

View File

@@ -44,6 +44,7 @@ export default function LoginButton({
id={trackingId} id={trackingId}
color={color} color={color}
href={`${login[lang]}?redirectTo=${encodeURIComponent(pathName)}`} href={`${login[lang]}?redirectTo=${encodeURIComponent(pathName)}`}
prefetch={false}
> >
{children} {children}
</Link> </Link>

View File

@@ -28,7 +28,7 @@ export default async function Header({
/** /**
* ToDo: Create logic to get this info from ContentStack based on page * ToDo: Create logic to get this info from ContentStack based on page
* */ * */
const hideBookingWidget = false const hideBookingWidget = true
if (!data) { if (!data) {
return null return null

View File

@@ -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;
}
}

4
components/Hero/hero.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface HeroProps {
alt: string
src: string
}

17
components/Hero/index.tsx Normal file
View File

@@ -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 (
<Image
className={styles.hero}
alt={alt}
height={480}
width={1196}
src={src}
/>
)
}

View File

@@ -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 (
<section className={styles.section}>
<div>
<Title textAlign="center" as="h2">
{intl.formatMessage({ id: "Thank you" })}
</Title>
<Subtitle textAlign="center" textTransform="uppercase">
{intl.formatMessage({ id: "We look forward to your visit!" })}
</Subtitle>
</div>
<Body color="burgundy" textAlign="center">
{intl.formatMessage({
id: "We have sent a detailed confirmation of your booking to your email: ",
})}
{email}
</Body>
<div className={styles.buttons}>
<Button
asChild
size="small"
theme="base"
intent="secondary"
className={styles.button}
>
<Link href="#" color="none">
{intl.formatMessage({ id: "Download the Scandic app" })}
</Link>
</Button>
<Button
asChild
size="small"
theme="base"
intent="secondary"
className={styles.button}
>
<Link href="#" color="none">
{intl.formatMessage({ id: "View your booking" })}
</Link>
</Button>
</div>
</section>
)
}

View File

@@ -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;
}
}

View File

@@ -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 (
<>
<section className={styles.card}>
<Image
src={hotel.image}
alt=""
height={400}
width={200}
className={styles.image}
/>
<div className={styles.info}>
<div className={styles.hotel}>
<ScandicLogoIcon color="red" />
<Title as="h5" textTransform="capitalize">
{hotel.name}
</Title>
<Caption color="burgundy" className={styles.caption}>
<span>{hotel.address}</span>
<span>{hotel.phone}</span>
</Caption>
</div>
<Body className={styles.stay}>
<span>{`${stay.nights} ${nightsText}`}</span>
<span className={styles.dates}>
<span>{stay.start}</span>
<ArrowRightIcon height={15} width={15} />
<span>{stay.end}</span>
</span>
</Body>
</div>
</section>
<section className={styles.table}>
<div className={styles.breakfast}>
<Body color="burgundy">
{intl.formatMessage({ id: "Breakfast" })}
</Body>
<Caption className={styles.caption}>
<span>{`${intl.formatMessage({ id: "Weekdays" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`}</span>
<span>{`${intl.formatMessage({ id: "Weekends" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`}</span>
</Caption>
</div>
<div className={styles.checkIn}>
<Body color="burgundy">{intl.formatMessage({ id: "Check in" })}</Body>
<Caption className={styles.caption}>
<span>{intl.formatMessage({ id: "From" })}</span>
<span>{hotel.checkIn}</span>
</Caption>
</div>
<div className={styles.checkOut}>
<Body color="burgundy">
{intl.formatMessage({ id: "Check out" })}
</Body>
<Caption className={styles.caption}>
<span>{intl.formatMessage({ id: "At latest" })}</span>
<span>{hotel.checkOut}</span>
</Caption>
</div>
</section>
</>
)
}

View File

@@ -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;
}
}

View File

@@ -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 (
<section className={styles.section}>
<Title as="h4" textAlign="center">
{intl.formatMessage({ id: "Summary" })}
</Title>
<Caption className={styles.summary}>
<span>{roomType}</span>
<span>1648 SEK</span>
</Caption>
<Caption className={styles.summary}>
<span>{bedType}</span>
<span>0 SEK</span>
</Caption>
<Caption className={styles.summary}>
<span>{breakfast}</span>
<span>198 SEK</span>
</Caption>
<Caption className={styles.summary}>
<span>{flexibility}</span>
<span>200 SEK</span>
</Caption>
</section>
)
}

View File

@@ -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);
}

View File

@@ -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",
},
}

View File

@@ -0,0 +1,11 @@
import { PropsWithChildren } from "react"
import styles from "./intro.module.css"
export default async function Intro({ children }: PropsWithChildren) {
return (
<div className={styles.intro}>
<div className={styles.content}>{children}</div>
</div>
)
}

View File

@@ -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);
}
}

View File

@@ -347,7 +347,9 @@ export const renderOptions: RenderOptions = {
const image = insertResponseToImageVaultAsset(attrs) const image = insertResponseToImageVaultAsset(attrs)
const alt = image.meta.alt ?? image.title 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) const props = extractPossibleAttributes(attrs)
return ( return (
<section key={node.uid}> <section key={node.uid}>

View File

@@ -3,7 +3,7 @@
{ {
"level": 1, "level": 1,
"name": "New Friend", "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.", "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", "icon": "/_static/icons/loyaltylevels/new-friend.svg",
"benefits": [ "benefits": [
@@ -78,7 +78,7 @@
{ {
"level": 2, "level": 2,
"name": "Good Friend", "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.", "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", "icon": "/_static/icons/loyaltylevels/good-friend.svg",
"benefits": [ "benefits": [
@@ -153,7 +153,7 @@
{ {
"level": 3, "level": 3,
"name": "Close Friend", "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.", "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", "icon": "/_static/icons/loyaltylevels/close-friend.svg",
"benefits": [ "benefits": [
@@ -229,7 +229,7 @@
{ {
"level": 4, "level": 4,
"name": "Dear Friend", "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.", "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", "icon": "/_static/icons/loyaltylevels/dear-friend.svg",
"benefits": [ "benefits": [
@@ -306,7 +306,7 @@
{ {
"level": 5, "level": 5,
"name": "Loyal Friend", "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.", "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", "icon": "/_static/icons/loyaltylevels/loyal-friend.svg",
"benefits": [ "benefits": [
@@ -383,7 +383,7 @@
{ {
"level": 6, "level": 6,
"name": "True Friend", "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.", "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", "icon": "/_static/icons/loyaltylevels/true-friend.svg",
"benefits": [ "benefits": [
@@ -460,7 +460,7 @@
{ {
"level": 7, "level": 7,
"name": "Best Friend", "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!", "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", "icon": "/_static/icons/loyaltylevels/best-friend.svg",
"benefits": [ "benefits": [

View File

@@ -3,29 +3,29 @@
{ {
"level": 1, "level": 1,
"name": "New Friend", "name": "New Friend",
"requirement": "0p", "requirement": "0 p",
"description": "Olemme uuden ja upean kynnyksellä: New Friend-ystävänä pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.", "description": "Ystävänämme pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.",
"icon": "/_static/icons/loyaltylevels/new-friend.svg", "icon": "/_static/icons/loyaltylevels/new-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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.", "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, "unlocked": true,
"value": "10%" "value": "10 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
@@ -39,38 +39,38 @@
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": false
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": false
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": false
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": false
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": false
} }
] ]
@@ -78,29 +78,29 @@
{ {
"level": 2, "level": 2,
"name": "Good Friend", "name": "Good Friend",
"requirement": "5 000p", "requirement": "5 000 p",
"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!", "description": "Tästä on hyvä jatkaa, yksi yöpyminen ja iloinen yllätys kerrallaan!",
"icon": "/_static/icons/loyaltylevels/good-friend.svg", "icon": "/_static/icons/loyaltylevels/good-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
@@ -114,38 +114,38 @@
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": false
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": false
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": false
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": false
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": false
} }
] ]
@@ -153,29 +153,29 @@
{ {
"level": 3, "level": 3,
"name": "Close Friend", "name": "Close Friend",
"requirement": "10 000p", "requirement": "10 000 p",
"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.", "description": "Nyt etusi vain paranevat, sillä olemmehan jo enemmän kuin hyvän päivän tuttuja.",
"icon": "/_static/icons/loyaltylevels/close-friend.svg", "icon": "/_static/icons/loyaltylevels/close-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
@@ -190,38 +190,38 @@
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": false
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": false
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": false
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": false
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": false
} }
] ]
@@ -229,29 +229,29 @@
{ {
"level": 4, "level": 4,
"name": "Dear Friend", "name": "Dear Friend",
"requirement": "25 000p", "requirement": "25 000 p",
"description": "Kippis syventyvälle ystävyydellemme. Nyt pääset nauttimaan liudasta uusia etuja.", "description": "Kippis syventyvälle ystävyydellemme. Nyt pääset nauttimaan liudasta uusia etuja.",
"icon": "/_static/icons/loyaltylevels/dear-friend.svg", "icon": "/_static/icons/loyaltylevels/dear-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
@@ -264,41 +264,41 @@
"name": "Enemmän pisteitä", "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.", "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, "unlocked": true,
"value": "25%" "value": "25 %"
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": false "unlocked": false
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": false
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": false
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": false
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": false
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": false
} }
] ]
@@ -306,29 +306,29 @@
{ {
"level": 5, "level": 5,
"name": "Loyal Friend", "name": "Loyal Friend",
"requirement": "100 000p", "requirement": "100 000 p",
"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.", "description": "Haluamme panostaa ystävyyteemme myös jatkossa ja annammekin sinulle kasan uusia, ihania etuja.",
"icon": "/_static/icons/loyaltylevels/loyal-friend.svg", "icon": "/_static/icons/loyaltylevels/loyal-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
@@ -341,41 +341,41 @@
"name": "Enemmän pisteitä", "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.", "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, "unlocked": true,
"value": "25%" "value": "25 %"
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": true
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": false
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": false
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": false
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": false
} }
] ]
@@ -383,29 +383,29 @@
{ {
"level": 6, "level": 6,
"name": "True Friend", "name": "True Friend",
"requirement": "250 000p", "requirement": "250 000 p",
"description": "Onpa ollut ihana nähdä sinua näin paljon viime aikoina. Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.", "description": "Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.",
"icon": "/_static/icons/loyaltylevels/true-friend.svg", "icon": "/_static/icons/loyaltylevels/true-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
@@ -416,43 +416,43 @@
}, },
{ {
"name": "Enemmän pisteitä", "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, "unlocked": true,
"value": "50%" "value": "50 %"
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": true
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": true
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": true
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": false
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": false
} }
] ]
@@ -460,76 +460,76 @@
{ {
"level": 7, "level": 7,
"name": "Best Friend", "name": "Best Friend",
"requirement": "400 000p tai 100 yötä", "requirement": "400 000 p 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.", "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", "icon": "/_static/icons/loyaltylevels/best-friend.svg",
"benefits": [ "benefits": [
{ {
"name": "Ystävähinnat", "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 "unlocked": true
}, },
{ {
"name": "Alennus ruoasta", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta", "name": "Mocktail lapsille maksutta",
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.", "description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan", "name": "Myöhäinen uloskirjautuminen",
"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.", "description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Ravintolakuponki", "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, "unlocked": true,
"value": "20 €" "value": "20 €"
}, },
{ {
"name": "Enemmän pisteitä", "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, "unlocked": true,
"value": "50%" "value": "50 %"
}, },
{ {
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan", "name": "Aikainen sisäänkirjautuminen",
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.", "description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan", "name": "Maksuton huoneluokan korotus",
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!", "description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
"unlocked": true "unlocked": true
}, },
{ {
"name": "Aamiainen kaksi yhden hinnalla", "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 "unlocked": true
}, },
{ {
"name": "48tunnin huonetakuu", "name": "48tunnin 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 "unlocked": true
}, },
{ {
"name": "Aamiainen aina maksutta", "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 "unlocked": true
}, },
{ {
"name": "Upea vuotuinen lahja", "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 "unlocked": true
}, },
{ {
"name": "Kids boost", "name": "Kids 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 "unlocked": true
} }
] ]

View File

@@ -16,7 +16,7 @@
"name": "Rabatt på mat", "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.", "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, "unlocked": true,
"value": "10%" "value": "10 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -34,7 +34,7 @@
"unlocked": false "unlocked": false
}, },
{ {
"name": "Ekstra vennskap", "name": "Friendsboost",
"description": "", "description": "",
"unlocked": false "unlocked": false
}, },
@@ -89,9 +89,9 @@
}, },
{ {
"name": "Rabatt på mat", "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 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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -109,7 +109,7 @@
"unlocked": false "unlocked": false
}, },
{ {
"name": "Ekstra vennskap", "name": "Friendsboost",
"description": "", "description": "",
"unlocked": false "unlocked": false
}, },
@@ -164,9 +164,9 @@
}, },
{ {
"name": "Rabatt på mat", "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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -185,7 +185,7 @@
"value": "50 NOK" "value": "50 NOK"
}, },
{ {
"name": "Ekstra vennskap", "name": "Friendsboost",
"description": "", "description": "",
"unlocked": false "unlocked": false
}, },
@@ -240,9 +240,9 @@
}, },
{ {
"name": "Rabatt på mat", "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 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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -261,10 +261,10 @@
"value": "75 NOK" "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.", "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, "unlocked": true,
"value": "25%" "value": "25 %"
}, },
{ {
"name": "Tidlig innsjekk når tilgjengelig", "name": "Tidlig innsjekk når tilgjengelig",
@@ -317,9 +317,9 @@
}, },
{ {
"name": "Rabatt på mat", "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 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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -338,10 +338,10 @@
"value": "100 NOK" "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.", "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, "unlocked": true,
"value": "25%" "value": "25 %"
}, },
{ {
"name": "Tidlig innsjekk når tilgjengelig", "name": "Tidlig innsjekk når tilgjengelig",
@@ -394,9 +394,9 @@
}, },
{ {
"name": "Rabatt på mat", "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 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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -415,10 +415,10 @@
"value": "150 NOK" "value": "150 NOK"
}, },
{ {
"name": "Ekstra vennskap", "name": "Friendsboost",
"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", "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, "unlocked": true,
"value": "50%" "value": "50 %"
}, },
{ {
"name": "Tidlig innsjekk når tilgjengelig", "name": "Tidlig innsjekk når tilgjengelig",
@@ -471,9 +471,9 @@
}, },
{ {
"name": "Rabatt på mat", "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 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, "unlocked": true,
"value": "15%" "value": "15 %"
}, },
{ {
"name": "Gratis barne-mocktail under oppholdet", "name": "Gratis barne-mocktail under oppholdet",
@@ -492,10 +492,10 @@
"value": "200 NOK" "value": "200 NOK"
}, },
{ {
"name": "Ekstra vennskap", "name": "Friendsboost",
"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", "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, "unlocked": true,
"value": "50%" "value": "50 %"
}, },
{ {
"name": "Tidlig innsjekk når tilgjengelig", "name": "Tidlig innsjekk når tilgjengelig",

View File

@@ -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 <MyPagesSidebar />
}

View File

@@ -1,7 +1,7 @@
import JsonToHtml from "@/components/JsonToHtml" import JsonToHtml from "@/components/JsonToHtml"
import SidebarMyPages from "@/components/MyPages/Sidebar"
import JoinLoyaltyContact from "./JoinLoyalty" import JoinLoyaltyContact from "./JoinLoyalty"
import { MyPagesNavigation } from "./MyPagesNavigation"
import styles from "./sidebar.module.css" import styles from "./sidebar.module.css"
@@ -38,7 +38,7 @@ export default function SidebarLoyalty({ blocks }: SidebarProps) {
case SidebarTypenameEnum.LoyaltyPageSidebarDynamicContent: case SidebarTypenameEnum.LoyaltyPageSidebarDynamicContent:
switch (block.dynamic_content.component) { switch (block.dynamic_content.component) {
case LoyaltySidebarDynamicComponentEnum.my_pages_navigation: case LoyaltySidebarDynamicComponentEnum.my_pages_navigation:
return <SidebarMyPages key={`${block.__typename}-${idx}`} /> return <MyPagesNavigation key={`${block.__typename}-${idx}`} />
default: default:
return null return null
} }

View File

@@ -31,7 +31,7 @@ export default async function Friend({
{formatMessage( {formatMessage(
isHighestLevel isHighestLevel
? { id: "Highest level" } ? { id: "Highest level" }
: { id: "Your current level" } : { id: `Level ${membershipLevels[membership.membershipLevel]}` }
)} )}
</Body> </Body>
{membership ? ( {membership ? (

View File

@@ -7,9 +7,8 @@ import { trpc } from "@/lib/trpc/client"
import LoadingSpinner from "@/components/LoadingSpinner" import LoadingSpinner from "@/components/LoadingSpinner"
import DesktopTable from "./Desktop"
import MobileTable from "./Mobile"
import Pagination from "./Pagination" import Pagination from "./Pagination"
import Table from "./Table"
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn" import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
@@ -40,8 +39,7 @@ export default function TransactionTable({
<LoadingSpinner /> <LoadingSpinner />
) : ( ) : (
<> <>
<MobileTable transactions={data?.data.transactions || []} /> <Table transactions={data?.data.transactions || []} />
<DesktopTable transactions={data?.data.transactions || []} />
{data && data.meta.totalPages > 1 ? ( {data && data.meta.totalPages > 1 ? (
<Pagination <Pagination
handlePageChange={setPage} handlePageChange={setPage}

View File

@@ -1,26 +0,0 @@
import { Lang } from "@/constants/languages"
import { awardPointsVariants } from "./awardPointsVariants"
import type {
AwardPointsProps,
AwardPointsVariantProps,
} from "@/types/components/myPages/myPage/earnAndBurn"
export default function AwardPoints({ awardPoints }: AwardPointsProps) {
let variant: AwardPointsVariantProps["variant"] = undefined
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 <td className={classNames}>{formatter.format(awardPoints)} pts</td>
}

View File

@@ -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 (
<tr className={styles.tr}>
<td className={styles.td}>{arrival}</td>
<td className={styles.td}>{description}</td>
<td className={styles.td}>{transaction.confirmationNumber}</td>
<td className={styles.td}>{departure}</td>
<AwardPoints awardPoints={transaction.awardPoints} />
</tr>
)
}

View File

@@ -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 (
<div className={styles.container}>
<table className={styles.table}>
<thead className={styles.thead}>
<tr>
<Body asChild>
<th className={styles.th}>
{intl.formatMessage({ id: "Transactions" })}
</th>
</Body>
<Body asChild>
<th className={styles.th}>
{intl.formatMessage({ id: "Points" })}
</th>
</Body>
</tr>
</thead>
<tbody>
{transactions.length ? (
transactions.map((transaction, idx) => (
<tr
className={styles.tr}
key={`${transaction.confirmationNumber}-${idx}`}
>
<td className={`${styles.td} ${styles.transactionDetails}`}>
<span className={styles.transactionDate}>
{dt(transaction.checkinDate)
.locale(lang)
.format("DD MMM YYYY")}
</span>
{transaction.hotelName && transaction.city ? (
<span>{`${transaction.hotelName}, ${transaction.city}`}</span>
) : null}
<span>
{`${transaction.nights} ${intl.formatMessage({ id: transaction.nights === 1 ? "night" : "nights" })}`}
</span>
</td>
<AwardPoints awardPoints={transaction.awardPoints} />
</tr>
))
) : (
<tr>
<td className={styles.placeholder} colSpan={2}>
{intl.formatMessage({
id: "There are no transactions to display",
})}
</td>
</tr>
)}
</tbody>
</table>
</div>
)
}

View File

@@ -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;
}
}

View File

@@ -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 (
<td className={classNames}>
{isCalculated
? formatter.format(awardPoints)
: intl.formatMessage({ id: "Points being calculated" })}
</td>
)
}

View File

@@ -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 (
<tr className={styles.tr}>
<AwardPoints
awardPoints={transaction.awardPoints}
isCalculated={transaction.pointsCalculated}
/>
<td className={`${styles.td} ${styles.description}`}>{description}</td>
<td className={styles.td}>
{transaction.type === RewardTransactionTypes.stay &&
transaction.bookingUrl ? (
<Link variant="underscored" href={transaction.bookingUrl}>
{transaction.confirmationNumber}
</Link>
) : (
transaction.confirmationNumber
)}
</td>
<td className={styles.td}>
{transaction.checkinDate ? arrival : transactionDate}
</td>
</tr>
)
}

View File

@@ -1,13 +1,21 @@
.tr { .tr {
border: 1px solid #e6e9ec; border-bottom: 1px solid var(--Scandic-Brand-Pale-Peach);
&:last-child {
border-bottom: none;
}
} }
.td { .td {
background-color: #fff; background-color: #fff;
color: var(--UI-Text-High-contrast); color: var(--UI-Text-High-contrast);
padding: var(--Spacing-x2) var(--Spacing-x4); padding: var(--Spacing-x2);
position: relative; position: relative;
text-align: left; text-align: left;
text-wrap: nowrap;
}
.description {
font-weight: var(--typography-Body-Bold-fontWeight);
} }
.addition { .addition {
@@ -17,8 +25,7 @@
.addition::before { .addition::before {
color: var(--Secondary-Light-On-Surface-Accent); color: var(--Secondary-Light-On-Surface-Accent);
content: "+"; content: "+";
left: var(--Spacing-x2); margin-right: var(--Spacing-x-half);
position: absolute;
} }
.negation { .negation {
@@ -28,6 +35,11 @@
.negation::before { .negation::before {
color: var(--Base-Text-Accent); color: var(--Base-Text-Accent);
content: "-"; content: "-";
left: var(--Spacing-x2); margin-right: var(--Spacing-x-half);
position: absolute; }
@media screen and (min-width: 768px) {
.td {
padding: var(--Spacing-x3);
}
} }

View File

@@ -1,28 +1,27 @@
"use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Row from "./Row" 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" import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
const tableHeadings = [ const tableHeadings = [
"Arrival date", "Points",
"Description", "Description",
"Booking number", "Booking number",
"Transaction date", "Arrival date",
"Points",
] ]
export default function DesktopTable({ transactions }: TableProps) { export default function Table({ transactions }: TableProps) {
const intl = useIntl() const intl = useIntl()
return ( return (
<div className={styles.container}> <div className={styles.container}>
{transactions.length ? ( {transactions.length ? (
<div>
<table className={styles.table}> <table className={styles.table}>
<thead className={styles.thead}> <thead className={styles.thead}>
<tr> <tr>
@@ -36,15 +35,14 @@ export default function DesktopTable({ transactions }: TableProps) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{transactions.map((transaction, idx) => ( {transactions.map((transaction, index) => (
<Row <Row
key={`${transaction.confirmationNumber}-${idx}`} key={`${transaction.confirmationNumber}-${index}`}
transaction={transaction} transaction={transaction}
/> />
))} ))}
</tbody> </tbody>
</table> </table>
</div>
) : ( ) : (
<table className={styles.table}> <table className={styles.table}>
<thead className={styles.thead}> <thead className={styles.thead}>

View File

@@ -1,5 +1,8 @@
.container { .container {
display: none; display: flex;
flex-direction: column;
overflow-x: auto;
border-radius: var(--Corner-radius-Small);
} }
.table { .table {
@@ -17,7 +20,8 @@
.th { .th {
text-align: left; text-align: left;
padding: 20px 32px; text-wrap: nowrap;
padding: var(--Spacing-x2);
} }
.placeholder { .placeholder {
@@ -49,9 +53,10 @@
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.container { .container {
display: flex; border-radius: var(--Corner-radius-Large);
flex-direction: column; }
gap: 16px;
overflow-x: auto; .th {
padding: var(--Spacing-x2) var(--Spacing-x3);
} }
} }

View File

@@ -11,6 +11,7 @@ export default async function Breadcrumbs() {
if (!breadcrumbs?.length) { if (!breadcrumbs?.length) {
return null return null
} }
const homeBreadcrumb = breadcrumbs.shift() const homeBreadcrumb = breadcrumbs.shift()
return ( return (
<nav className={styles.breadcrumbs}> <nav className={styles.breadcrumbs}>

View File

@@ -1,64 +1,81 @@
"use client" "use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useEffect } from "react" import { useEffect, useRef } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { toast } from "sonner"
import { trpc } from "@/lib/trpc/client" import { trpc } from "@/lib/trpc/client"
import { PlusCircleIcon } from "@/components/Icons" import { PlusCircleIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import styles from "./addCreditCardButton.module.css" import styles from "./addCreditCardButton.module.css"
import { type AddCreditCardButtonProps } from "@/types/components/myPages/myProfile/addCreditCardButton"
let hasRunOnce = false
function useAddCardResultToast() { function useAddCardResultToast() {
const hasRunOnce = useRef(false)
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
useEffect(() => { useEffect(() => {
if (hasRunOnce) return if (hasRunOnce.current) return
const success = searchParams.get("success") const success = searchParams.get("success")
const failure = searchParams.get("failure") const failure = searchParams.get("failure")
const cancel = searchParams.get("cancel")
const error = searchParams.get("error")
if (success) { if (success) {
// setTimeout is used to make sure DOM is loaded before triggering toast. See documentation for more info: https://sonner.emilkowal.ski/toast#render-toast-on-page-load
setTimeout(() => {
toast.success( toast.success(
intl.formatMessage({ id: "Your card was successfully saved!" }) intl.formatMessage({ id: "Your card was successfully saved!" })
) )
} else if (cancel) {
toast.warning(
intl.formatMessage({
id: "You canceled adding a new credit card.",
}) })
} else if (failure) { )
setTimeout(() => { } else if (failure || error) {
toast.error(intl.formatMessage({ id: "Something went wrong!" })) toast.error(
intl.formatMessage({
id: "Something went wrong and we couldn't add your card. Please try again later.",
}) })
)
} }
router.replace(pathname) router.replace(pathname)
hasRunOnce = true hasRunOnce.current = true
}, [intl, pathname, router, searchParams]) }, [intl, pathname, router, searchParams])
} }
export default function AddCreditCardButton({ export default function AddCreditCardButton() {
redirectUrl,
}: AddCreditCardButtonProps) {
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const lang = useLang() const lang = useLang()
useAddCardResultToast() useAddCardResultToast()
const initiateAddCard = trpc.user.initiateSaveCard.useMutation({ const initiateAddCard = trpc.user.creditCard.add.useMutation({
onSuccess: (result) => (result ? router.push(result.attribute.link) : null), onSuccess: (result) => {
onError: () => if (result?.attribute.link) {
toast.error(intl.formatMessage({ id: "Something went wrong!" })), router.push(result.attribute.link)
} else {
toast.error(
intl.formatMessage({
id: "We could not add a card right now, please try again later.",
})
)
}
},
onError: () => {
toast.error(
intl.formatMessage({
id: "An error occurred when adding a credit card, please try again later.",
})
)
},
}) })
return ( return (
@@ -70,8 +87,6 @@ export default function AddCreditCardButton({
onClick={() => onClick={() =>
initiateAddCard.mutate({ initiateAddCard.mutate({
language: lang, language: lang,
mobileToken: false,
redirectUrl,
}) })
} }
wrapping wrapping

View File

@@ -0,0 +1,4 @@
.cardContainer {
display: grid;
gap: var(--Spacing-x1);
}

View File

@@ -0,0 +1,31 @@
"use client"
import React from "react"
import { trpc } from "@/lib/trpc/client"
import CreditCardRow from "../CreditCardRow"
import styles from "./CreditCardList.module.css"
import type { CreditCard } from "@/types/user"
export default function CreditCardList({
initialData,
}: {
initialData?: CreditCard[] | null
}) {
const creditCards = trpc.user.creditCards.useQuery(undefined, { initialData })
if (!creditCards.data || !creditCards.data.length) {
return null
}
return (
<div className={styles.cardContainer}>
{creditCards.data.map((card) => (
<CreditCardRow key={card.id} card={card} />
))}
</div>
)
}

View File

@@ -0,0 +1,10 @@
.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);
}

View File

@@ -0,0 +1,22 @@
import { CreditCard } from "@/components/Icons"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import DeleteCreditCardConfirmation from "../DeleteCreditCardConfirmation"
import styles from "./creditCardRow.module.css"
import type { CreditCardRowProps } from "@/types/components/myPages/myProfile/creditCards"
export default function CreditCardRow({ card }: CreditCardRowProps) {
const maskedCardNumber = `**** ${card.truncatedNumber.slice(-4)}`
return (
<div className={styles.card}>
<CreditCard color="black" />
<Body textTransform="bold">{card.type}</Body>
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
<DeleteCreditCardConfirmation card={card} />
</div>
)
}

View File

@@ -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 (
<Button variant="icon" theme="base" intent="text" onClick={handleDelete}>
<Delete color="burgundy" />
</Button>
)
}

View File

@@ -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;
}
}

View File

@@ -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 (
<div>
<DialogTrigger>
<Button variant="icon" theme="base" intent="text">
<Delete color="burgundy" />
</Button>
<ModalOverlay className={styles.overlay} isDismissable>
<Modal className={styles.modal}>
<Dialog role="alertdialog">
{({ close }) => (
<div className={styles.container}>
<Heading slot="title" className={styles.title}>
{intl.formatMessage({
id: "Remove card from member profile",
})}
</Heading>
<p className={styles.bodyText}>
{`${intl.formatMessage({
id: "Are you sure you want to remove the card ending with",
})} ${lastFourDigits} ${intl.formatMessage({ id: "from your member profile?" })}`}
</p>
{deleteCard.isPending ? (
<LoadingSpinner />
) : (
<div className={styles.buttonContainer}>
<Button intent="secondary" theme="base" onClick={close}>
{intl.formatMessage({ id: "No, keep card" })}
</Button>
<Button
intent="primary"
theme="base"
onClick={() => {
deleteCard.mutate(
{ creditCardId: card.id },
{ onSettled: close }
)
}}
>
{intl.formatMessage({ id: "Yes, remove my card" })}
</Button>
</div>
)}
</div>
)}
</Dialog>
</Modal>
</ModalOverlay>
</DialogTrigger>
</div>
)
}

View File

@@ -1,9 +1,20 @@
import { buttonVariants } from "./variants" import { buttonVariants } from "./variants"
import type { VariantProps } from "class-variance-authority" import type { VariantProps } from "class-variance-authority"
import type { ButtonProps as ReactAriaButtonProps } from "react-aria-components"
export interface ButtonProps export interface ButtonPropsRAC
extends Omit<ReactAriaButtonProps, "isDisabled">,
VariantProps<typeof buttonVariants> {
asChild?: false | undefined | never
disabled?: ReactAriaButtonProps["isDisabled"]
onClick?: ReactAriaButtonProps["onPress"]
}
export interface ButtonPropsSlot
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild: true
} }
export type ButtonProps = ButtonPropsSlot | ButtonPropsRAC

View File

@@ -1,23 +1,16 @@
"use client" "use client"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { Button as ButtonRAC } from "react-aria-components"
import { buttonVariants } from "./variants" import { buttonVariants } from "./variants"
import type { ButtonProps } from "./button" import type { ButtonProps } from "./button"
export default function Button({ export default function Button(props: ButtonProps) {
asChild = false, const { className, intent, size, theme, wrapping, variant, ...restProps } =
theme, props
className,
disabled,
intent,
size,
variant,
wrapping,
...props
}: ButtonProps) {
const Comp = asChild ? Slot : "button"
const classNames = buttonVariants({ const classNames = buttonVariants({
className, className,
intent, intent,
@@ -26,5 +19,19 @@ export default function Button({
wrapping, wrapping,
variant, variant,
}) })
return <Comp className={classNames} disabled={disabled} {...props} />
if (restProps.asChild) {
const { asChild, ...slotProps } = restProps
return <Slot className={classNames} {...slotProps} />
}
const { asChild, onClick, disabled, ...racProps } = restProps
return (
<ButtonRAC
className={classNames}
isDisabled={disabled}
onPress={onClick}
{...racProps}
/>
)
} }

View File

@@ -2,7 +2,7 @@ import { loyaltyCardVariants } from "./variants"
import type { VariantProps } from "class-variance-authority" import type { VariantProps } from "class-variance-authority"
import { ImageVaultAsset } from "@/types/components/imageVaultImage" import { ImageVaultAsset } from "@/types/components/imageVault"
export interface LoyaltyCardProps export interface LoyaltyCardProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,

View File

@@ -16,7 +16,7 @@ import { toastVariants } from "./variants"
import styles from "./toasts.module.css" import styles from "./toasts.module.css"
export function ToastHandler() { export function ToastHandler() {
return <Toaster /> return <Toaster position="bottom-right" />
} }
function getIcon(variant: ToastsProps["variant"]) { function getIcon(variant: ToastsProps["variant"]) {

View File

@@ -31,8 +31,9 @@
.iconContainer { .iconContainer {
display: flex; display: flex;
background-color: var(--icon-background-color);
padding: var(--Spacing-x2);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: var(--icon-background-color);
padding: var(--Spacing-x2);
height: 100%;
} }

View File

@@ -12,6 +12,16 @@ export const login = {
sv: "/sv/logga-in", 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} */ /** @type {import('@/types/routes').LangRoute} */
export const logout = { export const logout = {
da: "/da/log-ud", da: "/da/log-ud",
@@ -22,6 +32,16 @@ export const logout = {
sv: "/sv/logga-ut", 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} */ /** @type {import('@/types/routes').LangRoute} */
export const verifymagiclink = { export const verifymagiclink = {
da: "/da/verifymagiclink", da: "/da/verifymagiclink",
@@ -36,4 +56,6 @@ export const handleAuth = [
...Object.values(login), ...Object.values(login),
...Object.values(logout), ...Object.values(logout),
...Object.values(verifymagiclink), ...Object.values(verifymagiclink),
...Object.values(loginUnLocalized),
...Object.values(logoutUnLocalized),
] ]

View File

@@ -7,6 +7,8 @@
"All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter", "All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter",
"Already a friend?": "Allerede en ven?", "Already a friend?": "Allerede en ven?",
"Amenities": "Faciliteter", "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", "Arrival date": "Ankomstdato",
"as of today": "fra idag", "as of today": "fra idag",
"As our": "Som vores", "As our": "Som vores",
@@ -31,6 +33,7 @@
"Could not find requested resource": "Kunne ikke finde den anmodede ressource", "Could not find requested resource": "Kunne ikke finde den anmodede ressource",
"Country": "Land", "Country": "Land",
"Country code": "Landekode", "Country code": "Landekode",
"Credit card deleted successfully": "Kreditkort blev slettet",
"Your current level": "Dit nuværende niveau", "Your current level": "Dit nuværende niveau",
"Current password": "Nuværende kodeord", "Current password": "Nuværende kodeord",
"characters": "tegn", "characters": "tegn",
@@ -42,13 +45,24 @@
"Edit": "Redigere", "Edit": "Redigere",
"Edit profile": "Rediger profil", "Edit profile": "Rediger profil",
"Email": "E-mail", "Email": "E-mail",
"Extras to your booking": "Ekstra til din booking",
"There are no transactions to display": "Der er ingen transaktioner at vise", "There are no transactions to display": "Der er ingen transaktioner at vise",
"Explore all levels and benefits": "Udforsk alle niveauer og fordele", "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", "Find booking": "Find booking",
"Flexibility": "Fleksibilitet", "Flexibility": "Fleksibilitet",
"Former Scandic Hotel": "Tidligere Scandic Hotel",
"From": "Fra", "From": "Fra",
"from your member profile?": "fra din medlemsprofil?",
"Get inspired": "Bliv inspireret", "Get inspired": "Bliv inspireret",
"Go back to overview": "Gå tilbage til oversigten", "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", "Highest level": "Højeste niveau",
"How do you want to sleep?": "Hvordan vil du sove?", "How do you want to sleep?": "Hvordan vil du sove?",
"How it works": "Hvordan det virker", "How it works": "Hvordan det virker",
@@ -73,6 +87,7 @@
"Next": "Næste", "Next": "Næste",
"next level:": "Næste niveau:", "next level:": "Næste niveau:",
"No content published": "Intet indhold offentliggjort", "No content published": "Intet indhold offentliggjort",
"No, keep card": "Nej, behold kortet",
"No transactions available": "Ingen tilgængelige transaktioner", "No transactions available": "Ingen tilgængelige transaktioner",
"Not found": "Ikke fundet", "Not found": "Ikke fundet",
"night": "nat", "night": "nat",
@@ -88,7 +103,9 @@
"Phone is required": "Telefonnummer er påkrævet", "Phone is required": "Telefonnummer er påkrævet",
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",
"Please enter a valid phone number": "Indtast venligst et gyldigt 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 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 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", "Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
@@ -96,10 +113,13 @@
"Previous victories": "Tidligere sejre", "Previous victories": "Tidligere sejre",
"Read more": "Læs mere", "Read more": "Læs mere",
"Read more about the hotel": "Læs mere om hotellet", "Read more about the hotel": "Læs mere om hotellet",
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
"Restaurant & Bar": "Restaurant & Bar", "Restaurant & Bar": "Restaurant & Bar",
"Retype new password": "Gentag den nye adgangskode", "Retype new password": "Gentag den nye adgangskode",
"Rooms": "Værelser", "Rooms": "Værelser",
"Save": "Gemme", "Save": "Gemme",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Select a country": "Vælg et land", "Select a country": "Vælg et land",
"Select country of residence": "Vælg bopælsland", "Select country of residence": "Vælg bopælsland",
"Select date of birth": "Vælg fødselsdato", "Select date of birth": "Vælg fødselsdato",
@@ -108,7 +128,10 @@
"Show more": "Vis mere", "Show more": "Vis mere",
"Show all amenities": "Vis alle faciliteter", "Show all amenities": "Vis alle faciliteter",
"Skip to main content": "Spring over og gå til hovedindhold", "Skip to main content": "Spring over og gå til hovedindhold",
"Sign up bonus": "Tilmeldingsbonus",
"Something went wrong!": "Noget gik galt!", "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", "Street": "Gade",
"special character": "speciel karakter", "special character": "speciel karakter",
"Total Points": "Samlet antal point", "Total Points": "Samlet antal point",
@@ -117,17 +140,22 @@
"Transactions": "Transaktioner", "Transactions": "Transaktioner",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
"to": "til", "to": "til",
"TUI Points": "TUI-point",
"User information": "Brugeroplysninger", "User information": "Brugeroplysninger",
"uppercase letter": "stort bogstav", "uppercase letter": "stort bogstav",
"Visiting address": "Besøgsadresse", "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": "Velkommen",
"Welcome to": "Velkommen til", "Welcome to": "Velkommen til",
"Wellness & Exercise": "Velvære & Motion", "Wellness & Exercise": "Velvære & Motion",
"Where should you go next?": "Find inspiration til dit næste ophold", "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", "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig",
"Year": "År", "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 previous stays.": "Du har ingen tidligere ophold.",
"You have no upcoming stays.": "Du har ingen kommende 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 card was successfully saved!": "Dit kort blev gemt!",
"Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!", "Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!",
"Your level": "Dit niveau", "Your level": "Dit niveau",
@@ -136,6 +164,19 @@
"Hotel facilities": "Hotel faciliteter", "Hotel facilities": "Hotel faciliteter",
"Hotel surroundings": "Hotel omgivelser", "Hotel surroundings": "Hotel omgivelser",
"Show map": "Vis kort", "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", "Where to": "Hvorhen",
"When": "Hvornår", "When": "Hvornår",
"Rooms & Guests": "Værelser & gæster", "Rooms & Guests": "Værelser & gæster",

View File

@@ -6,8 +6,10 @@
"All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet", "All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet",
"Already a friend?": "Sind wir schon Freunde?", "Already a friend?": "Sind wir schon Freunde?",
"Amenities": "Annehmlichkeiten", "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", "Arrival date": "Ankunftsdatum",
"as of today": "Ab heute", "as of today": "Stand heute",
"As our": "Als unser", "As our": "Als unser",
"As our Close Friend": "Als unser enger Freund", "As our Close Friend": "Als unser enger Freund",
"At the hotel": "Im Hotel", "At the hotel": "Im Hotel",
@@ -30,6 +32,7 @@
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.", "Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
"Country": "Land", "Country": "Land",
"Country code": "Landesvorwahl", "Country code": "Landesvorwahl",
"Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht",
"Your current level": "Ihr aktuelles Level", "Your current level": "Ihr aktuelles Level",
"Current password": "Aktuelles Passwort", "Current password": "Aktuelles Passwort",
"characters": "figuren", "characters": "figuren",
@@ -41,13 +44,24 @@
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Edit profile": "Profil bearbeiten", "Edit profile": "Profil bearbeiten",
"Email": "Email", "Email": "Email",
"Extras to your booking": "Extras zu Ihrer Buchung",
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", "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", "Find booking": "Buchung finden",
"Flexibility": "Flexibilität", "Flexibility": "Flexibilität",
"Former Scandic Hotel": "Ehemaliges Scandic Hotel",
"From": "Fromm", "From": "Fromm",
"from your member profile?": "wirklich aus Ihrem Mitgliedsprofil entfernen?",
"Get inspired": "Lassen Sie sich inspieren", "Get inspired": "Lassen Sie sich inspieren",
"Go back to overview": "Zurück zur Übersicht", "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", "Highest level": "Höchstes Level",
"How do you want to sleep?": "Wie möchtest du schlafen?", "How do you want to sleep?": "Wie möchtest du schlafen?",
"How it works": "Wie es funktioniert", "How it works": "Wie es funktioniert",
@@ -71,6 +85,7 @@
"Next": "Nächste", "Next": "Nächste",
"next level:": "Nächstes Level:", "next level:": "Nächstes Level:",
"No content published": "Kein Inhalt veröffentlicht", "No content published": "Kein Inhalt veröffentlicht",
"No, keep card": "Nein, Karte behalten",
"No transactions available": "Keine Transaktionen verfügbar", "No transactions available": "Keine Transaktionen verfügbar",
"Not found": "Nicht gefunden", "Not found": "Nicht gefunden",
"night": "nacht", "night": "nacht",
@@ -86,15 +101,20 @@
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",
"Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein",
"Points": "Punkte", "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 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 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", "spendable points expiring by": "Einlösbare punkte verfallen bis zum",
"Previous victories": "Bisherige Siege", "Previous victories": "Bisherige Siege",
"Read more": "Mehr lesen", "Read more": "Mehr lesen",
"Read more about the hotel": "Lesen Sie mehr über das Hotel", "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", "Retype new password": "Neues Passwort erneut eingeben",
"Save": "Speichern", "Save": "Speichern",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Select a country": "Wähle ein Land", "Select a country": "Wähle ein Land",
"Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus",
"Select date of birth": "Geburtsdatum auswählen", "Select date of birth": "Geburtsdatum auswählen",
@@ -103,25 +123,33 @@
"Show more": "Mehr anzeigen", "Show more": "Mehr anzeigen",
"Show all amenities": "Alle Annehmlichkeiten anzeigen", "Show all amenities": "Alle Annehmlichkeiten anzeigen",
"Skip to main content": "Direkt zum Inhalt", "Skip to main content": "Direkt zum Inhalt",
"Sign up bonus": "Anmeldebonus",
"Something went wrong!": "Etwas ist schief gelaufen!", "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", "Street": "Straße",
"special character": "sonderzeichen", "special character": "sonderzeichen",
"Total Points": "Gesamtpunktzahl", "Total Points": "Gesamtpunktzahl",
"Your points to spend": "Deine Punkte", "Your points to spend": "Meine Punkte",
"Transaction date": "Transaktionsdatum", "Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen", "Transactions": "Transaktionen",
"Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)", "Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)",
"to": "zu", "to": "zu",
"TUI Points": "TUI Punkte",
"User information": "Nutzerinformation", "User information": "Nutzerinformation",
"uppercase letter": "großbuchstabe", "uppercase letter": "großbuchstabe",
"Visiting address": "Besuchsadresse", "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 to": "Willkommen zu",
"Welcome": "Willkommen", "Welcome": "Willkommen",
"Where should you go next?": "Wo geht es als Nächstes hin?", "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?", "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?",
"Year": "Jahr", "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 previous stays.": "Sie haben keine vorherigen Aufenthalte.",
"You have no upcoming stays.": "Sie haben keine bevorstehenden 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 card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!",
"Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!", "Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!",
"Your level": "Dein level", "Your level": "Dein level",
@@ -130,6 +158,19 @@
"Hotel facilities": "Hotel-Infos", "Hotel facilities": "Hotel-Infos",
"Hotel surroundings": "Umgebung des Hotels", "Hotel surroundings": "Umgebung des Hotels",
"Show map": "Karte anzeigen", "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", "Where to": "Wohin",
"When": "Wann", "When": "Wann",
"Rooms & Guests": "Zimmer & Gäste", "Rooms & Guests": "Zimmer & Gäste",

View File

@@ -7,6 +7,8 @@
"All rooms comes with standard amenities": "All rooms comes with standard amenities", "All rooms comes with standard amenities": "All rooms comes with standard amenities",
"Already a friend?": "Already a friend?", "Already a friend?": "Already a friend?",
"Amenities": "Amenities", "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", "Arrival date": "Arrival date",
"as of today": "as of today", "as of today": "as of today",
"As our": "As our", "As our": "As our",
@@ -34,6 +36,7 @@
"Your current level": "Your current level", "Your current level": "Your current level",
"Current password": "Current password", "Current password": "Current password",
"characters": "characters", "characters": "characters",
"Credit card deleted successfully": "Credit card deleted successfully",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Day": "Day", "Day": "Day",
"Description": "Description", "Description": "Description",
@@ -45,15 +48,26 @@
"Email": "Email", "Email": "Email",
"There are no transactions to display": "There are no transactions to display", "There are no transactions to display": "There are no transactions to display",
"Explore all levels and benefits": "Explore all levels and benefits", "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", "FAQ": "FAQ",
"Find booking": "Find booking", "Find booking": "Find booking",
"Former Scandic Hotel": "Former Scandic Hotel",
"Flexibility": "Flexibility", "Flexibility": "Flexibility",
"From": "From", "From": "From",
"from your member profile?": "from your member profile?",
"Get inspired": "Get inspired", "Get inspired": "Get inspired",
"Go back to overview": "Go back to overview", "Go back to overview": "Go back to overview",
"hotelPages.rooms.roomCard.person": "person", "hotelPages.rooms.roomCard.person": "person",
"hotelPages.rooms.roomCard.persons": "persons", "hotelPages.rooms.roomCard.persons": "persons",
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "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", "Highest level": "Highest level",
"How do you want to sleep?": "How do you want to sleep?", "How do you want to sleep?": "How do you want to sleep?",
"How it works": "How it works", "How it works": "How it works",
@@ -78,6 +92,7 @@
"Next": "Next", "Next": "Next",
"next level:": "next level:", "next level:": "next level:",
"No content published": "No content published", "No content published": "No content published",
"No, keep card": "No, keep card",
"No transactions available": "No transactions available", "No transactions available": "No transactions available",
"Not found": "Not found", "Not found": "Not found",
"night": "night", "night": "night",
@@ -94,6 +109,8 @@
"Phone number": "Phone number", "Phone number": "Phone number",
"Please enter a valid phone number": "Please enter a valid phone number", "Please enter a valid phone number": "Please enter a valid phone number",
"Points": "Points", "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 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 level up": "Points needed to level up",
"Points needed to stay on level": "Points needed to stay on level", "Points needed to stay on level": "Points needed to stay on level",
@@ -101,11 +118,14 @@
"Previous victories": "Previous victories", "Previous victories": "Previous victories",
"Read more": "Read more", "Read more": "Read more",
"Read more about the hotel": "Read more about the hotel", "Read more about the hotel": "Read more about the hotel",
"Remove card from member profile": "Remove card from member profile",
"Restaurant & Bar": "Restaurant & Bar", "Restaurant & Bar": "Restaurant & Bar",
"Retype new password": "Retype new password", "Retype new password": "Retype new password",
"Rooms": "Rooms", "Rooms": "Rooms",
"Save": "Save", "Save": "Save",
"See room details": "See room details", "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 a country": "Select a country",
"Select country of residence": "Select country of residence", "Select country of residence": "Select country of residence",
"Select date of birth": "Select date of birth", "Select date of birth": "Select date of birth",
@@ -114,26 +134,34 @@
"Show more": "Show more", "Show more": "Show more",
"Show all amenities": "Show all amenities", "Show all amenities": "Show all amenities",
"Skip to main content": "Skip to main content", "Skip to main content": "Skip to main content",
"Sign up bonus": "Sign up bonus",
"Something went wrong!": "Something went wrong!", "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", "Street": "Street",
"special character": "special character", "special character": "special character",
"Total Points": "Total Points", "Total Points": "Total Points",
"Your points to spend": "Your points to spend", "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", "Transaction date": "Transaction date",
"Transactions": "Transactions", "Transactions": "Transactions",
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)", "Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
"to": "to", "to": "to",
"TUI Points": "TUI Points",
"User information": "User information", "User information": "User information",
"uppercase letter": "uppercase letter", "uppercase letter": "uppercase letter",
"Welcome": "Welcome", "Welcome": "Welcome",
"Visiting address": "Visiting address", "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", "Welcome to": "Welcome to",
"Wellness & Exercise": "Wellness & Exercise", "Wellness & Exercise": "Wellness & Exercise",
"Where should you go next?": "Where should you go next?", "Where should you go next?": "Where should you go next?",
"Which room class suits you the best?": "Which room class suits you the best?", "Which room class suits you the best?": "Which room class suits you the best?",
"Year": "Year", "Year": "Year",
"Yes, remove my card": "Yes, remove my card",
"You have no previous stays.": "You have no previous stays.", "You have no previous stays.": "You have no previous stays.",
"You have no upcoming stays.": "You have no upcoming 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 card was successfully saved!": "Your card was successfully saved!",
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!", "Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
"Your level": "Your level", "Your level": "Your level",
@@ -142,6 +170,19 @@
"Hotel facilities": "Hotel facilities", "Hotel facilities": "Hotel facilities",
"Hotel surroundings": "Hotel surroundings", "Hotel surroundings": "Hotel surroundings",
"Show map": "Show map", "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", "Where to": "Where to",
"When": "When", "When": "When",
"Rooms & Guests": "Rooms & Guests", "Rooms & Guests": "Rooms & Guests",

View File

@@ -7,6 +7,8 @@
"All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet", "All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet",
"Already a friend?": "Oletko jo ystävä?", "Already a friend?": "Oletko jo ystävä?",
"Amenities": "Mukavuudet", "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ä", "Arrival date": "Saapumispäivä",
"as of today": "tästä päivästä lähtien", "as of today": "tästä päivästä lähtien",
"As our": "Kuin meidän", "As our": "Kuin meidän",
@@ -31,6 +33,7 @@
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt", "Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
"Country": "Maa", "Country": "Maa",
"Country code": "Maatunnus", "Country code": "Maatunnus",
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
"Your current level": "Nykyinen tasosi", "Your current level": "Nykyinen tasosi",
"Current password": "Nykyinen salasana", "Current password": "Nykyinen salasana",
"characters": "hahmoja", "characters": "hahmoja",
@@ -42,13 +45,24 @@
"Edit": "Muokata", "Edit": "Muokata",
"Edit profile": "Muokkaa profiilia", "Edit profile": "Muokkaa profiilia",
"Email": "Sähköposti", "Email": "Sähköposti",
"Extras to your booking": "Lisävarusteet varaukseesi",
"There are no transactions to display": "Näytettäviä tapahtumia ei ole", "There are no transactions to display": "Näytettäviä tapahtumia ei ole",
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "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", "Find booking": "Etsi varaus",
"Flexibility": "Joustavuus", "Flexibility": "Joustavuus",
"Former Scandic Hotel": "Entinen Scandic Hotel",
"From": "From", "From": "From",
"from your member profile?": "jäsenprofiilistasi?",
"Get inspired": "Inspiroidu", "Get inspired": "Inspiroidu",
"Go back to overview": "Palaa yleiskatsaukseen", "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", "Highest level": "Korkein taso",
"How do you want to sleep?": "Kuinka haluat nukkua?", "How do you want to sleep?": "Kuinka haluat nukkua?",
"How it works": "Kuinka se toimii", "How it works": "Kuinka se toimii",
@@ -73,6 +87,7 @@
"Next": "Seuraava", "Next": "Seuraava",
"next level:": "Seuraava taso:", "next level:": "Seuraava taso:",
"No content published": "Ei julkaistua sisältöä", "No content published": "Ei julkaistua sisältöä",
"No, keep card": "Ei, pidä kortti",
"No transactions available": "Ei tapahtumia saatavilla", "No transactions available": "Ei tapahtumia saatavilla",
"Not found": "Ei löydetty", "Not found": "Ei löydetty",
"night": "yö", "night": "yö",
@@ -89,6 +104,8 @@
"Phone number": "Puhelinnumero", "Phone number": "Puhelinnumero",
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
"Points": "Pistettä", "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 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 level up": "Pisteitä tarvitaan tasolle pääsemiseksi",
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
@@ -96,10 +113,13 @@
"Previous victories": "Edelliset voitot", "Previous victories": "Edelliset voitot",
"Read more": "Lue lisää", "Read more": "Lue lisää",
"Read more about the hotel": "Lue lisää hotellista", "Read more about the hotel": "Lue lisää hotellista",
"Remove card from member profile": "Poista kortti jäsenprofiilista",
"Restaurant & Bar": "Ravintola & Baari", "Restaurant & Bar": "Ravintola & Baari",
"Retype new password": "Kirjoita uusi salasana uudelleen", "Retype new password": "Kirjoita uusi salasana uudelleen",
"Rooms": "Huoneet", "Rooms": "Huoneet",
"Save": "Tallentaa", "Save": "Tallentaa",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Select a country": "Valitse maa", "Select a country": "Valitse maa",
"Select country of residence": "Valitse asuinmaa", "Select country of residence": "Valitse asuinmaa",
"Select date of birth": "Valitse syntymäaika", "Select date of birth": "Valitse syntymäaika",
@@ -108,7 +128,10 @@
"Show more": "Näytä lisää", "Show more": "Näytä lisää",
"Show all amenities": "Näytä kaikki mukavuudet", "Show all amenities": "Näytä kaikki mukavuudet",
"Skip to main content": "Siirry pääsisältöön", "Skip to main content": "Siirry pääsisältöön",
"Sign up bonus": "Rekisteröidy bonus",
"Something went wrong!": "Jotain meni pieleen!", "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", "Street": "Katu",
"special character": "erikoishahmo", "special character": "erikoishahmo",
"Total Points": "Kokonaispisteet", "Total Points": "Kokonaispisteet",
@@ -117,18 +140,23 @@
"Transactions": "Tapahtumat", "Transactions": "Tapahtumat",
"Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)", "Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)",
"to": "to", "to": "to",
"TUI Points": "TUI-pisteet",
"User information": "Käyttäjän tiedot", "User information": "Käyttäjän tiedot",
"uppercase letter": "iso kirjain", "uppercase letter": "iso kirjain",
"Visiting address": "Käyntiosoite", "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": "Tervetuloa",
"Welcome to": "Tervetuloa", "Welcome to": "Tervetuloa",
"Wellness & Exercise": "Hyvinvointi & Liikunta", "Wellness & Exercise": "Hyvinvointi & Liikunta",
"Where should you go next?": "Mihin menisit seuraavaksi?", "Where should you go next?": "Mihin menisit seuraavaksi?",
"Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?",
"Year": "Vuosi", "Year": "Vuosi",
"Yes, remove my card": "Kyllä, poista korttini",
"You have no previous stays.": "Sinulla ei ole aiempaa oleskelua.", "You have no previous stays.": "Sinulla ei ole aiempaa oleskelua.",
"You have no upcoming stays.": "Sinulla ei ole tulevia oleskeluja.", "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!", "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 Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!",
"Your level": "Tasosi", "Your level": "Tasosi",
"Zip code": "Postinumero", "Zip code": "Postinumero",
@@ -136,6 +164,19 @@
"Hotel facilities": "Hotellin palvelut", "Hotel facilities": "Hotellin palvelut",
"Hotel surroundings": "Hotellin ympäristö", "Hotel surroundings": "Hotellin ympäristö",
"Show map": "Näytä kartta", "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", "Where to": "Minne",
"When": "Kun", "When": "Kun",
"Rooms & Guestss": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat",

View File

@@ -7,12 +7,14 @@
"All rooms comes with standard amenities": "Alle rommene har standard fasiliteter", "All rooms comes with standard amenities": "Alle rommene har standard fasiliteter",
"Already a friend?": "Allerede Friend?", "Already a friend?": "Allerede Friend?",
"Amenities": "Fasiliteter", "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", "Arrival date": "Ankomstdato",
"as of today": "per idag", "as of today": "per idag",
"As our": "Som vår", "As our": "Som vår",
"As our Close Friend": "Som vår nære venn", "As our Close Friend": "Som vår nære venn",
"At the hotel": "På hotellet", "At the hotel": "På hotellet",
"Book": "Bok", "Book": "Bestill",
"Booking number": "Bestillingsnummer", "Booking number": "Bestillingsnummer",
"Breakfast": "Frokost", "Breakfast": "Frokost",
"by": "innen", "by": "innen",
@@ -34,6 +36,7 @@
"Your current level": "Ditt nåværende nivå", "Your current level": "Ditt nåværende nivå",
"Current password": "Nåværende passord", "Current password": "Nåværende passord",
"characters": "tegn", "characters": "tegn",
"Credit card deleted successfully": "Kredittkort slettet",
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
"Day": "Dag", "Day": "Dag",
"Description": "Beskrivelse", "Description": "Beskrivelse",
@@ -42,13 +45,24 @@
"Edit": "Redigere", "Edit": "Redigere",
"Edit profile": "Rediger profil", "Edit profile": "Rediger profil",
"Email": "E-post", "Email": "E-post",
"Extras to your booking": "Ekstra til din bestilling",
"There are no transactions to display": "Det er ingen transaksjoner å vise", "There are no transactions to display": "Det er ingen transaksjoner å vise",
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "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", "Find booking": "Finn booking",
"Flexibility": "Fleksibilitet", "Flexibility": "Fleksibilitet",
"Former Scandic Hotel": "Tidligere Scandic Hotel",
"From": "Fra", "From": "Fra",
"from your member profile?": "fra medlemsprofilen din?",
"Get inspired": "Bli inspirert", "Get inspired": "Bli inspirert",
"Go back to overview": "Gå tilbake til oversikten", "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å", "Highest level": "Høyeste nivå",
"How do you want to sleep?": "Hvordan vil du sove?", "How do you want to sleep?": "Hvordan vil du sove?",
"How it works": "Hvordan det fungerer", "How it works": "Hvordan det fungerer",
@@ -73,6 +87,7 @@
"Next": "Neste", "Next": "Neste",
"next level:": "Neste nivå:", "next level:": "Neste nivå:",
"No content published": "Ingen innhold publisert", "No content published": "Ingen innhold publisert",
"No, keep card": "Nei, behold kortet",
"No transactions available": "Ingen transaksjoner tilgjengelig", "No transactions available": "Ingen transaksjoner tilgjengelig",
"Not found": "Ikke funnet", "Not found": "Ikke funnet",
"night": "natt", "night": "natt",
@@ -89,6 +104,8 @@
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",
"Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer", "Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer",
"Points": "Poeng", "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 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 level up": "Poeng som trengs for å komme opp i nivå",
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
@@ -96,10 +113,13 @@
"Previous victories": "Tidligere seire", "Previous victories": "Tidligere seire",
"Read more": "Les mer", "Read more": "Les mer",
"Read more about the hotel": "Les mer om hotellet", "Read more about the hotel": "Les mer om hotellet",
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
"Restaurant & Bar": "Restaurant & Bar", "Restaurant & Bar": "Restaurant & Bar",
"Retype new password": "Skriv inn nytt passord på nytt", "Retype new password": "Skriv inn nytt passord på nytt",
"Rooms": "Rom", "Rooms": "Rom",
"Save": "Lagre", "Save": "Lagre",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Select a country": "Velg et land", "Select a country": "Velg et land",
"Select country of residence": "Velg bostedsland", "Select country of residence": "Velg bostedsland",
"Select date of birth": "Velg fødselsdato", "Select date of birth": "Velg fødselsdato",
@@ -108,7 +128,10 @@
"Show more": "Vis mer", "Show more": "Vis mer",
"Show all amenities": "Vis alle fasiliteter", "Show all amenities": "Vis alle fasiliteter",
"Skip to main content": "Gå videre til hovedsiden", "Skip to main content": "Gå videre til hovedsiden",
"Sign up bonus": "Registreringsbonus",
"Something went wrong!": "Noe gikk galt!", "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", "Street": "Gate",
"special character": "spesiell karakter", "special character": "spesiell karakter",
"Total Points": "Totale poeng", "Total Points": "Totale poeng",
@@ -117,17 +140,22 @@
"Transactions": "Transaksjoner", "Transactions": "Transaksjoner",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)", "Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
"to": "til", "to": "til",
"TUI Points": "TUI-poeng",
"User information": "Brukerinformasjon", "User information": "Brukerinformasjon",
"uppercase letter": "stor bokstav", "uppercase letter": "stor bokstav",
"Visiting address": "Besøksadresse", "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": "Velkommen",
"Welcome to": "Velkommen til", "Welcome to": "Velkommen til",
"Wellness & Exercise": "Velvære & Trening", "Wellness & Exercise": "Velvære & Trening",
"Where should you go next?": "Hvor ønsker du å reise neste gang?", "Where should you go next?": "Hvor ønsker du å reise neste gang?",
"Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Which room class suits you the best?": "Hvilken romklasse passer deg best?",
"Year": "År", "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 previous stays.": "Du har ingen tidligere opphold.",
"You have no upcoming stays.": "Du har ingen kommende 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 card was successfully saved!": "Kortet ditt ble lagret!",
"Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!", "Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!",
"Your level": "Ditt nivå", "Your level": "Ditt nivå",
@@ -136,6 +164,19 @@
"Hotel facilities": "Hotelfaciliteter", "Hotel facilities": "Hotelfaciliteter",
"Hotel surroundings": "Hotellomgivelser", "Hotel surroundings": "Hotellomgivelser",
"Show map": "Vis kart", "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", "Where to": "Hvor skal du",
"When": "Når", "When": "Når",
"Rooms & Guests": "Rom og gjester", "Rooms & Guests": "Rom og gjester",

View File

@@ -7,6 +7,8 @@
"All rooms comes with standard amenities": "Alla rum har standardbekvämligheter", "All rooms comes with standard amenities": "Alla rum har standardbekvämligheter",
"Already a friend?": "Är du redan en vän?", "Already a friend?": "Är du redan en vän?",
"Amenities": "Bekvämligheter", "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", "Arrival date": "Ankomstdatum",
"as of today": "från och med idag", "as of today": "från och med idag",
"As our": "Som vår", "As our": "Som vår",
@@ -31,6 +33,7 @@
"Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Could not find requested resource": "Det gick inte att hitta den begärda resursen",
"Country": "Land", "Country": "Land",
"Country code": "Landskod", "Country code": "Landskod",
"Credit card deleted successfully": "Kreditkort har tagits bort",
"Your current level": "Din nuvarande nivå", "Your current level": "Din nuvarande nivå",
"Current password": "Nuvarande lösenord", "Current password": "Nuvarande lösenord",
"characters": "tecken", "characters": "tecken",
@@ -42,13 +45,24 @@
"Edit": "Redigera", "Edit": "Redigera",
"Edit profile": "Redigera profil", "Edit profile": "Redigera profil",
"Email": "E-post", "Email": "E-post",
"Extras to your booking": "Extra till din bokning",
"There are no transactions to display": "Det finns inga transaktioner att visa", "There are no transactions to display": "Det finns inga transaktioner att visa",
"Explore all levels and benefits": "Utforska alla nivåer och fördelar", "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", "Find booking": "Hitta bokning",
"Flexibility": "Flexibilitet", "Flexibility": "Flexibilitet",
"Former Scandic Hotel": "Tidigare Scandic Hotel",
"From": "Från", "From": "Från",
"from your member profile?": "från din medlemsprofil?",
"Get inspired": "Bli inspirerad", "Get inspired": "Bli inspirerad",
"Go back to overview": "Gå tillbaka till översikten", "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å", "Highest level": "Högsta nivå",
"How do you want to sleep?": "Hur vill du sova?", "How do you want to sleep?": "Hur vill du sova?",
"How it works": "Hur det fungerar", "How it works": "Hur det fungerar",
@@ -76,6 +90,7 @@
"Next": "Nästa", "Next": "Nästa",
"next level:": "Nästa nivå:", "next level:": "Nästa nivå:",
"No content published": "Inget innehåll publicerat", "No content published": "Inget innehåll publicerat",
"No, keep card": "Nej, behåll kortet",
"No transactions available": "Inga transaktioner tillgängliga", "No transactions available": "Inga transaktioner tillgängliga",
"Not found": "Hittades inte", "Not found": "Hittades inte",
"night": "natt", "night": "natt",
@@ -92,6 +107,8 @@
"Phone number": "Telefonnummer", "Phone number": "Telefonnummer",
"Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer", "Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer",
"Points": "Poäng", "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 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 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å", "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", "Previous victories": "Tidigare segrar",
"Read more": "Läs mer", "Read more": "Läs mer",
"Read more about the hotel": "Läs mer om hotellet", "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", "Restaurant & Bar": "Restaurang & Bar",
"Retype new password": "Upprepa nytt lösenord", "Retype new password": "Upprepa nytt lösenord",
"Rooms": "Rum", "Rooms": "Rum",
"Save": "Spara", "Save": "Spara",
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
"Select a country": "Välj ett land", "Select a country": "Välj ett land",
"Select country of residence": "Välj bosättningsland", "Select country of residence": "Välj bosättningsland",
"Select date of birth": "Välj födelsedatum", "Select date of birth": "Välj födelsedatum",
@@ -111,7 +131,10 @@
"Show more": "Visa mer", "Show more": "Visa mer",
"Show all amenities": "Visa alla bekvämligheter", "Show all amenities": "Visa alla bekvämligheter",
"Skip to main content": "Fortsätt till huvudinnehåll", "Skip to main content": "Fortsätt till huvudinnehåll",
"Sign up bonus": "Registreringsbonus",
"Something went wrong!": "Något gick fel!", "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", "Street": "Gata",
"special character": "speciell karaktär", "special character": "speciell karaktär",
"Total Points": "Poäng totalt", "Total Points": "Poäng totalt",
@@ -120,16 +143,21 @@
"Transactions": "Transaktioner", "Transactions": "Transaktioner",
"Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)", "Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)",
"to": "till", "to": "till",
"TUI Points": "TUI-poäng",
"User information": "Användar information", "User information": "Användar information",
"uppercase letter": "stor bokstav", "uppercase letter": "stor bokstav",
"Visiting address": "Besöksadress", "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", "Welcome": "Välkommen",
"Wellness & Exercise": "Hälsa & Träning", "Wellness & Exercise": "Hälsa & Träning",
"Where should you go next?": "Låter inte en spontanweekend härligt?", "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?", "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?",
"Year": "År", "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 previous stays.": "Du har inga tidigare vistelser.",
"You have no upcoming stays.": "Du har inga planerade resor.", "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 card was successfully saved!": "Ditt kort har sparats!",
"Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!", "Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!",
"Your level": "Din nivå", "Your level": "Din nivå",
@@ -138,6 +166,19 @@
"Hotel facilities": "Hotellfaciliteter", "Hotel facilities": "Hotellfaciliteter",
"Hotel surroundings": "Hotellomgivning", "Hotel surroundings": "Hotellomgivning",
"Show map": "Visa karta", "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", "Where to": "Vart",
"When": "När", "When": "När",
"Rooms & Guests": "Rum och gäster", "Rooms & Guests": "Rum och gäster",

View File

@@ -13,6 +13,8 @@ export namespace endpoints {
upcomingStays = "booking/v1/Stays/future", upcomingStays = "booking/v1/Stays/future",
previousStays = "booking/v1/Stays/past", previousStays = "booking/v1/Stays/past",
hotels = "hotel/v1/Hotels", hotels = "hotel/v1/Hotels",
intiateSaveCard = `${creditCards}/initiateSaveCard`,
deleteCreditCard = `${profile}/creditCards`,
} }
} }

View File

@@ -27,7 +27,7 @@ const fetch = fetchRetry(global.fetch, {
}) })
export async function get( export async function get(
endpoint: Endpoint | `${endpoints.v1.hotels}/${string}`, endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithOutBody, options: RequestOptionsWithOutBody,
params?: URLSearchParams params?: URLSearchParams
) { ) {
@@ -38,7 +38,7 @@ export async function get(
} }
export async function patch( export async function patch(
endpoint: Endpoint, endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithJSONBody options: RequestOptionsWithJSONBody
) { ) {
const { body, ...requestOptions } = options const { body, ...requestOptions } = options
@@ -54,11 +54,12 @@ export async function patch(
export async function post( export async function post(
endpoint: Endpoint | `${Endpoint}/${string}`, endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithJSONBody options: RequestOptionsWithJSONBody,
params?: URLSearchParams
) { ) {
const { body, ...requestOptions } = options const { body, ...requestOptions } = options
return fetch( return fetch(
`${env.API_BASEURL}/${endpoint}`, `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
merge.all([ merge.all([
defaultOptions, defaultOptions,
{ body: JSON.stringify(body), method: "POST" }, { body: JSON.stringify(body), method: "POST" },
@@ -68,11 +69,12 @@ export async function post(
} }
export async function remove( export async function remove(
endpoint: Endpoint, endpoint: Endpoint | `${Endpoint}/${string}`,
options: RequestOptionsWithOutBody options: RequestOptionsWithOutBody,
params?: URLSearchParams
) { ) {
return fetch( return fetch(
`${env.API_BASEURL}/${endpoint}`, `${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
merge.all([defaultOptions, { method: "DELETE" }, options]) merge.all([defaultOptions, { method: "DELETE" }, options])
) )
} }

View File

@@ -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
}
}
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
#import "../Image.graphql"
fragment LoyaltyPageMetaData on LoyaltyPage {
web {
seo_metadata {
title
description
imageConnection {
edges {
node {
...Image
}
}
}
}
breadcrumbs {
title
}
}
}

View File

@@ -0,0 +1,20 @@
#import "../Image.graphql"
fragment MyPagesMetaData on AccountPage {
web {
seo_metadata {
title
description
imageConnection {
edges {
node {
...Image
}
}
}
}
breadcrumbs {
title
}
}
}

View File

@@ -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
}
}

View File

@@ -1,4 +1,4 @@
#import "./System.graphql" #import "../System.graphql"
fragment ContentPageRef on ContentPage { fragment ContentPageRef on ContentPage {
system { system {

View File

@@ -3,7 +3,7 @@
#import "../Fragments/MyPages/AccountPage/AccountPageContentTextContent.graphql" #import "../Fragments/MyPages/AccountPage/AccountPageContentTextContent.graphql"
#import "../Fragments/Refs/MyPages/AccountPage.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/LoyaltyPage/LoyaltyPage.graphql"
#import "../Fragments/Refs/System.graphql" #import "../Fragments/Refs/System.graphql"

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -5,13 +5,12 @@
#import "../Fragments/Blocks/Refs/Card.graphql" #import "../Fragments/Blocks/Refs/Card.graphql"
#import "../Fragments/Blocks/Refs/LoyaltyCard.graphql" #import "../Fragments/Blocks/Refs/LoyaltyCard.graphql"
#import "../Fragments/LoyaltyPage/Breadcrumbs.graphql"
#import "../Fragments/PageLink/AccountPageLink.graphql" #import "../Fragments/PageLink/AccountPageLink.graphql"
#import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/Refs/MyPages/AccountPage.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/LoyaltyPage/LoyaltyPage.graphql"
#import "../Fragments/Refs/System.graphql" #import "../Fragments/Refs/System.graphql"
@@ -107,6 +106,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
} }
title title
heading heading
preamble
hero_image
sidebar { sidebar {
__typename __typename
... on LoyaltyPageSidebarDynamicContent { ... on LoyaltyPageSidebarDynamicContent {
@@ -167,7 +168,6 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
} }
} }
} }
...LoyaltyPageBreadcrumbs
system { system {
uid uid
created_at created_at

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -2,7 +2,7 @@
#import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/ContentPageLink.graphql"
#import "../Fragments/PageLink/LoyaltyPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../Fragments/Refs/MyPages/AccountPage.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/LoyaltyPage/LoyaltyPage.graphql"
#import "../Fragments/Refs/System.graphql" #import "../Fragments/Refs/System.graphql"

View File

@@ -50,9 +50,9 @@ export async function request<T>(
const nr = Math.random() const nr = Math.random()
console.log(`START REQUEST ${nr}`) console.log(`START REQUEST ${nr}`)
console.time(`OUTGOING REQUEST ${nr}`) console.time(`OUTGOING REQUEST ${nr}`)
console.log(`Sending reqeust to ${env.CMS_URL}`) // console.log(`Sending reqeust to ${env.CMS_URL}`)
console.log(`Query:`, print(query as DocumentNode)) // console.log(`Query:`, print(query as DocumentNode))
console.log(`Variables:`, variables) // console.log(`Variables:`, variables)
const response = await client.request<T>({ const response = await client.request<T>({
document: query, document: query,
@@ -64,7 +64,7 @@ export async function request<T>(
}) })
console.timeEnd(`OUTGOING REQUEST ${nr}`) console.timeEnd(`OUTGOING REQUEST ${nr}`)
console.log({ response }) // console.log({ response })
return { data: response } return { data: response }
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,9 @@
import { createTRPCReact } from "@trpc/react-query" import { createTRPCReact } from "@trpc/react-query"
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"
import type { AppRouter } from "@/server" import type { AppRouter } from "@/server"
export const trpc = createTRPCReact<AppRouter>() export const trpc = createTRPCReact<AppRouter>()
export type RouterInput = inferRouterInputs<AppRouter>
export type RouterOutput = inferRouterOutputs<AppRouter>

View File

@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
import { login } from "@/constants/routes/handleAuth"
import { webviews } from "@/constants/routes/webviews" import { webviews } from "@/constants/routes/webviews"
import { appRouter } from "@/server" import { appRouter } from "@/server"
import { createContext } from "@/server/context" import { createContext } from "@/server/context"
@@ -13,12 +14,11 @@ const createCaller = createCallerFactory(appRouter)
export function serverClient() { export function serverClient() {
return createCaller(createContext(), { return createCaller(createContext(), {
onError: ({ ctx, error, input, path, type }) => { onError: ({ ctx, error, input, path, type }) => {
console.error(`Server Client error for ${type}: ${path}`) console.error(`[serverClient] error for ${type}: ${path}`, error)
if (input) { if (input) {
console.error(`Received input:`) console.error(`[serverClient] received input:`, input)
console.error(input)
} }
console.error(error)
if (error instanceof TRPCError) { if (error instanceof TRPCError) {
if (error.code === "UNAUTHORIZED") { if (error.code === "UNAUTHORIZED") {
@@ -41,12 +41,13 @@ export function serverClient() {
redirectUrl redirectUrl
) )
console.log(`[serverClient] onError redirecting to: ${redirectUrl}`)
redirect(redirectUrl) redirect(redirectUrl)
} }
redirect( const redirectUrl = `${login[lang]}?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
`/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}` console.log(`[serverClient] onError redirecting to: ${redirectUrl}`)
) redirect(redirectUrl)
} }
} }

View File

@@ -49,9 +49,9 @@ export const middleware = auth(async (request) => {
} }
const publicUrl = new URL(env.PUBLIC_URL) const publicUrl = new URL(env.PUBLIC_URL)
const nextUrlClone = nextUrl.clone() const nextUrlPublic = nextUrl.clone()
nextUrlClone.host = publicUrl.host nextUrlPublic.host = publicUrl.host
nextUrlClone.hostname = publicUrl.hostname nextUrlPublic.hostname = publicUrl.hostname
/** /**
* Function to validate MFA from token data * Function to validate MFA from token data
@@ -67,8 +67,8 @@ export const middleware = auth(async (request) => {
if (isLoggedIn && isMFAPath && isMFAInvalid()) { if (isLoggedIn && isMFAPath && isMFAInvalid()) {
const headers = new Headers(request.headers) const headers = new Headers(request.headers)
headers.set("x-mfa-login", "true") headers.set("x-returnurl", nextUrlPublic.href)
headers.set("x-returnurl", nextUrlClone.href) headers.set("x-login-source", "mfa")
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
request: { request: {
headers, headers,
@@ -87,13 +87,16 @@ export const middleware = auth(async (request) => {
const headers = new Headers() const headers = new Headers()
headers.append( headers.append(
"set-cookie", "set-cookie",
`redirectTo=${encodeURIComponent(nextUrlClone.href)}; Path=/; HttpOnly; SameSite=Lax` `redirectTo=${encodeURIComponent(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax`
) )
const loginUrl = login[lang] const loginUrl = login[lang]
return NextResponse.redirect(new URL(loginUrl, nextUrlClone), { const redirectUrl = new URL(loginUrl, nextUrlPublic)
const redirectOpts = {
headers, headers,
}) }
console.log(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts)
return NextResponse.redirect(redirectUrl, redirectOpts)
}) as NextMiddleware // See comment above }) as NextMiddleware // See comment above
export const matcher: MiddlewareMatcher = (request) => { export const matcher: MiddlewareMatcher = (request) => {

View File

@@ -19,6 +19,7 @@ export const middleware: NextMiddleware = (request) => {
const headers = new Headers(request.headers) const headers = new Headers(request.headers)
headers.set("x-returnurl", returnUrl) headers.set("x-returnurl", returnUrl)
headers.set("x-login-source", "seamless")
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
request: { request: {

View File

@@ -19,7 +19,7 @@ export const middleware: NextMiddleware = (request) => {
const headers = new Headers(request.headers) const headers = new Headers(request.headers)
headers.set("x-returnurl", returnUrl) 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), { return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
request: { request: {

View File

@@ -23,6 +23,7 @@ export const middleware: NextMiddleware = (request) => {
const headers = new Headers(request.headers) const headers = new Headers(request.headers)
headers.set("x-returnurl", redirectTo) headers.set("x-returnurl", redirectTo)
headers.set("x-logout-source", "seamless")
return NextResponse.rewrite(new URL(`/${lang}/logout`, request.nextUrl), { return NextResponse.rewrite(new URL(`/${lang}/logout`, request.nextUrl), {
request: { request: {

View File

@@ -34,7 +34,9 @@ export const middleware: NextMiddleware = async (request) => {
nextUrlClone.hostname = publicUrl.hostname nextUrlClone.hostname = publicUrl.hostname
const overviewUrl = overview[lang] 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}`, "") const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")

View File

@@ -1,3 +1,6 @@
import { env } from "@/env/server"
import { internalServerError } from "@/server/errors/next"
import { findLang } from "@/utils/languages" import { findLang } from "@/utils/languages"
import { removeTrailingSlash } from "@/utils/url" import { removeTrailingSlash } from "@/utils/url"
@@ -6,6 +9,17 @@ import type { NextRequest } from "next/server"
export function getDefaultRequestHeaders(request: NextRequest) { export function getDefaultRequestHeaders(request: NextRequest) {
const lang = findLang(request.nextUrl.pathname)! 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) const headers = new Headers(request.headers)
headers.set("x-lang", lang) headers.set("x-lang", lang)
headers.set( headers.set(
@@ -14,7 +28,7 @@ export function getDefaultRequestHeaders(request: NextRequest) {
request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "") request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "")
) )
) )
headers.set("x-url", removeTrailingSlash(request.nextUrl.href)) headers.set("x-url", removeTrailingSlash(nextUrl.href))
return headers return headers
} }

Some files were not shown because too many files have changed in this diff Show More