Merge branch 'develop' into feat/SW-185-implement-footer-navigation
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
8
auth.ts
8
auth.ts
@@ -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 }) {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export default async function ContentPage() {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
19
components/ContentType/ContentPage/contentPage.module.css
Normal file
19
components/ContentType/ContentPage/contentPage.module.css
Normal 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);
|
||||||
|
}
|
||||||
46
components/ContentType/ContentPage/index.tsx
Normal file
46
components/ContentType/ContentPage/index.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
14
components/Hero/hero.module.css
Normal file
14
components/Hero/hero.module.css
Normal 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
4
components/Hero/hero.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface HeroProps {
|
||||||
|
alt: string
|
||||||
|
src: string
|
||||||
|
}
|
||||||
17
components/Hero/index.tsx
Normal file
17
components/Hero/index.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
||||||
11
components/Intro/index.tsx
Normal file
11
components/Intro/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
components/Intro/intro.module.css
Normal file
16
components/Intro/intro.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": false
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": false
|
"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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": false
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": false
|
"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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": false
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": false
|
"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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": false
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": false
|
"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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": false
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": false
|
"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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": true
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": false
|
"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": "48 tunnin huonetakuu",
|
"name": "48 tunnin huonetakuu",
|
||||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||||
"unlocked": true
|
"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": "Kid’s boost",
|
"name": "Kid’s boost",
|
||||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||||
"unlocked": true
|
"unlocked": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||||
"unlocked": true,
|
"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 på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||||
"unlocked": true,
|
"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 på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||||
"unlocked": true,
|
"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 på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||||
"unlocked": true,
|
"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 på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||||
"unlocked": true,
|
"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",
|
||||||
|
|||||||
13
components/Loyalty/Sidebar/MyPagesNavigation/index.tsx
Normal file
13
components/Loyalty/Sidebar/MyPagesNavigation/index.tsx
Normal 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 />
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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}>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.cardContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
31
components/Profile/CreditCardList/index.tsx
Normal file
31
components/Profile/CreditCardList/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
components/Profile/CreditCardRow/creditCardRow.module.css
Normal file
10
components/Profile/CreditCardRow/creditCardRow.module.css
Normal 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);
|
||||||
|
}
|
||||||
22
components/Profile/CreditCardRow/index.tsx
Normal file
22
components/Profile/CreditCardRow/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
components/Profile/DeleteCreditCardButton/index.tsx
Normal file
40
components/Profile/DeleteCreditCardButton/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
components/Profile/DeleteCreditCardConfirmation/index.tsx
Normal file
99
components/Profile/DeleteCreditCardConfirmation/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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"]) {
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
37
lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql
Normal file
37
lib/graphql/Fragments/ContentPage/Breadcrumbs.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/graphql/Fragments/LoyaltyPage/MetaData.graphql
Normal file
20
lib/graphql/Fragments/LoyaltyPage/MetaData.graphql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#import "../Image.graphql"
|
||||||
|
|
||||||
|
fragment LoyaltyPageMetaData on LoyaltyPage {
|
||||||
|
web {
|
||||||
|
seo_metadata {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
imageConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
breadcrumbs {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/graphql/Fragments/MyPages/MetaData.graphql
Normal file
20
lib/graphql/Fragments/MyPages/MetaData.graphql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#import "../Image.graphql"
|
||||||
|
|
||||||
|
fragment MyPagesMetaData on AccountPage {
|
||||||
|
web {
|
||||||
|
seo_metadata {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
imageConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...Image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
breadcrumbs {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
lib/graphql/Fragments/Refs/ContentPage/Breadcrumbs.graphql
Normal file
38
lib/graphql/Fragments/Refs/ContentPage/Breadcrumbs.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#import "./System.graphql"
|
#import "../System.graphql"
|
||||||
|
|
||||||
fragment ContentPageRef on ContentPage {
|
fragment ContentPageRef on ContentPage {
|
||||||
system {
|
system {
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
21
lib/graphql/Query/BreadcrumbsContentPage.graphql
Normal file
21
lib/graphql/Query/BreadcrumbsContentPage.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lib/graphql/Query/ContentPage.graphql
Normal file
52
lib/graphql/Query/ContentPage.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
12
lib/graphql/Query/MetaDataLoyaltyPage.graphql
Normal file
12
lib/graphql/Query/MetaDataLoyaltyPage.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/graphql/Query/MetaDataMyPages.graphql
Normal file
12
lib/graphql/Query/MetaDataMyPages.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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}`, "")
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user