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_SECRET_USER="test"
|
||||
CURITY_ISSUER_USER="test"
|
||||
CURITY_ISSUER_SERVICE="test"
|
||||
CYPRESS_API_BASEURL="test"
|
||||
CYPRESS_CURITY_USERNAME="test"
|
||||
CYPRESS_CURITY_PASSWORD="test"
|
||||
@@ -35,3 +36,4 @@ SEAMLESS_LOGOUT_FI="test"
|
||||
SEAMLESS_LOGOUT_NO="test"
|
||||
SEAMLESS_LOGOUT_SV="test"
|
||||
WEBVIEW_ENCRYPTION_KEY="test"
|
||||
BOOKING_ENCRYPTION_KEY="test"
|
||||
|
||||
@@ -20,13 +20,17 @@ export default async function ProtectedLayout({
|
||||
h.get("x-url") ?? h.get("x-pathname") ?? overview[getLang()]
|
||||
)
|
||||
|
||||
const redirectURL = `/${getLang()}/login?redirectTo=${redirectTo}`
|
||||
|
||||
if (!session) {
|
||||
redirect(`/${getLang()}/login?redirectTo=${redirectTo}`)
|
||||
console.log(`[layout:protected] no session, redirecting to: ${redirectURL}`)
|
||||
redirect(redirectURL)
|
||||
}
|
||||
|
||||
const user = await serverClient().user.get()
|
||||
if (!user || "error" in user) {
|
||||
redirect(`/${getLang()}/login?redirectTo=${redirectTo}`)
|
||||
console.log(`[layout:protected] no user, redirecting to: ${redirectURL}`)
|
||||
redirect(redirectURL)
|
||||
}
|
||||
|
||||
return children
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { createActionURL } from "@auth/core"
|
||||
import { headers as nextHeaders } from "next/headers"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { AuthError } from "next-auth"
|
||||
|
||||
@@ -16,11 +14,35 @@ export async function GET(
|
||||
let redirectTo: string = ""
|
||||
|
||||
const returnUrl = request.headers.get("x-returnurl")
|
||||
const isSeamless = request.headers.get("x-logout-source") === "seamless"
|
||||
|
||||
if (returnUrl) {
|
||||
// Seamless logout request from Current web
|
||||
redirectTo = returnUrl
|
||||
console.log(
|
||||
`[logout] source: ${request.headers.get("x-logout-source") || "normal"}`
|
||||
)
|
||||
|
||||
const redirectToSearchParamValue =
|
||||
request.nextUrl.searchParams.get("redirectTo")
|
||||
const redirectToFallback = "/"
|
||||
|
||||
if (isSeamless) {
|
||||
if (returnUrl) {
|
||||
redirectTo = returnUrl
|
||||
} else {
|
||||
console.log(
|
||||
`[login] missing returnUrl, using fallback: ${redirectToFallback}`
|
||||
)
|
||||
redirectTo = redirectToFallback
|
||||
}
|
||||
} else {
|
||||
redirectTo = redirectToSearchParamValue || redirectToFallback
|
||||
|
||||
// Make relative URL to absolute URL
|
||||
if (redirectTo.startsWith("/")) {
|
||||
console.log(`[logout] make redirectTo absolute, from ${redirectTo}`)
|
||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
||||
console.log(`[logout] make redirectTo absolute, to ${redirectTo}`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Initiate the seamless logout flow
|
||||
let redirectUrlValue
|
||||
@@ -45,6 +67,9 @@ export async function GET(
|
||||
break
|
||||
}
|
||||
const redirectUrl = new URL(redirectUrlValue)
|
||||
console.log(
|
||||
`[logout] creating redirect to seamless logout: ${redirectUrl}`
|
||||
)
|
||||
redirectTo = redirectUrl.toString()
|
||||
} catch (e) {
|
||||
console.error(
|
||||
@@ -55,37 +80,25 @@ export async function GET(
|
||||
}
|
||||
|
||||
try {
|
||||
redirectTo = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}`
|
||||
console.log(`[logout] final redirectUrl: ${redirectTo}`)
|
||||
console.log({ logout_env: process.env })
|
||||
|
||||
/**
|
||||
* Passing `redirect: false` to `signOut` will return a result object
|
||||
* instead of automatically redirecting inside of `signOut`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L104
|
||||
*/
|
||||
console.log({ logout_NEXTAUTH_URL: process.env.NEXTAUTH_URL })
|
||||
console.log({ logout_env: process.env })
|
||||
|
||||
const headers = new Headers(nextHeaders())
|
||||
const signOutURL = createActionURL(
|
||||
"signout",
|
||||
// @ts-expect-error `x-forwarded-proto` is not nullable, next.js sets it by default
|
||||
headers.get("x-forwarded-proto"),
|
||||
headers,
|
||||
process.env
|
||||
)
|
||||
|
||||
console.log({ logout_signOutURL: signOutURL })
|
||||
|
||||
// Redirect to Curity logout
|
||||
const curityLogoutUrl = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}`
|
||||
|
||||
console.log({ logout_redirectTo: curityLogoutUrl })
|
||||
|
||||
const redirectUrlObj = await signOut({
|
||||
redirectTo: curityLogoutUrl,
|
||||
redirectTo,
|
||||
redirect: false,
|
||||
})
|
||||
|
||||
if (redirectUrlObj) {
|
||||
console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`)
|
||||
return NextResponse.redirect(redirectUrlObj.redirect)
|
||||
} else {
|
||||
console.error(`[logout] missing redirectUrlObj reponse from signOut()`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -34,7 +34,6 @@ export default async function MyPages({
|
||||
<p>{formatMessage({ id: "No content published" })}</p>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<TrackingSDK pageData={tracking} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -14,17 +14,6 @@
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
column-gap: var(--Spacing-x1);
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
justify-items: flex-end;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
gap: var(--Spacing-x3);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { env } from "@/env/server"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { CreditCard, Delete } from "@/components/Icons"
|
||||
import AddCreditCardButton from "@/components/Profile/AddCreditCardButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CreditCardList from "@/components/Profile/CreditCardList"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
@@ -33,40 +31,8 @@ export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
|
||||
})}
|
||||
</Body>
|
||||
</article>
|
||||
{creditCards?.length ? (
|
||||
<div className={styles.cardContainer}>
|
||||
{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}`}
|
||||
/>
|
||||
<CreditCardList initialData={creditCards} />
|
||||
<AddCreditCardButton />
|
||||
</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}
|
||||
<Divider color="burgundy" opacity={8} />
|
||||
{creditCards}
|
||||
{communication}
|
||||
{/* TODO: Implement communication preferences flow. Hidden until decided on where to send user. */}
|
||||
{/* {communication} */}
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import ContentPage from "@/components/ContentType/ContentPage"
|
||||
import HotelPage from "@/components/ContentType/HotelPage/HotelPage"
|
||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage/LoyaltyPage"
|
||||
import HotelPage from "@/components/ContentType/HotelPage"
|
||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import {
|
||||
|
||||
@@ -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,
|
||||
context: { params: { lang: Lang } }
|
||||
) {
|
||||
let redirectHeaders: Headers | undefined = undefined
|
||||
let redirectTo: string
|
||||
|
||||
const returnUrl = request.headers.get("x-returnurl")
|
||||
const isMFA = request.headers.get("x-mfa-login")
|
||||
|
||||
// This is to support seamless login when using magic link login
|
||||
const isMagicLinkUpdateLogin = !!request.headers.get("x-magic-link")
|
||||
|
||||
if (!env.PUBLIC_URL) {
|
||||
throw internalServerError("No value for env.PUBLIC_URL")
|
||||
}
|
||||
|
||||
if (returnUrl) {
|
||||
// Seamless login request from Current web
|
||||
redirectTo = returnUrl
|
||||
let redirectHeaders: Headers | undefined = undefined
|
||||
let redirectTo: string
|
||||
|
||||
const returnUrl = request.headers.get("x-returnurl")
|
||||
const isSeamless = request.headers.get("x-login-source") === "seamless"
|
||||
const isMFA = request.headers.get("x-login-source") === "mfa"
|
||||
const isSeamlessMagicLink =
|
||||
request.headers.get("x-login-source") === "seamless-magiclink"
|
||||
|
||||
console.log(
|
||||
`[login] source: ${request.headers.get("x-login-source") || "normal"}`
|
||||
)
|
||||
|
||||
const redirectToCookieValue = request.cookies.get("redirectTo")?.value // Cookie gets set by authRequired middleware
|
||||
const redirectToSearchParamValue =
|
||||
request.nextUrl.searchParams.get("redirectTo")
|
||||
const redirectToFallback = "/"
|
||||
|
||||
console.log(`[login] redirectTo cookie value: ${redirectToCookieValue}`)
|
||||
console.log(
|
||||
`[login] redirectTo search param value: ${redirectToSearchParamValue}`
|
||||
)
|
||||
|
||||
if (isSeamless || isSeamlessMagicLink) {
|
||||
if (returnUrl) {
|
||||
redirectTo = returnUrl
|
||||
} else {
|
||||
console.log(
|
||||
`[login] missing returnUrl, using fallback: ${redirectToFallback}`
|
||||
)
|
||||
redirectTo = redirectToFallback
|
||||
}
|
||||
} else {
|
||||
// Normal login request from New web
|
||||
redirectTo =
|
||||
request.cookies.get("redirectTo")?.value || // Cookie gets set by authRequired middleware
|
||||
request.nextUrl.searchParams.get("redirectTo") ||
|
||||
"/"
|
||||
redirectToCookieValue || redirectToSearchParamValue || redirectToFallback
|
||||
|
||||
// Make relative URL to absolute URL
|
||||
if (redirectTo.startsWith("/")) {
|
||||
console.log(`[login] make redirectTo absolute, from ${redirectTo}`)
|
||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
||||
console.log(`[login] make redirectTo absolute, to ${redirectTo}`)
|
||||
}
|
||||
|
||||
// Clean up cookie from authRequired middleware
|
||||
@@ -70,7 +89,11 @@ export async function GET(
|
||||
break
|
||||
}
|
||||
const redirectUrl = new URL(redirectUrlValue)
|
||||
console.log(`[login] creating redirect to seamless login: ${redirectUrl}`)
|
||||
redirectUrl.searchParams.set("returnurl", redirectTo)
|
||||
console.log(
|
||||
`[login] returnurl for seamless login: ${redirectUrl.searchParams.get("returnurl")}`
|
||||
)
|
||||
redirectTo = redirectUrl.toString()
|
||||
|
||||
/** Set cookie with redirect Url to appropriately redirect user when using magic link login */
|
||||
@@ -82,25 +105,20 @@ export async function GET(
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Unable to create URL for seamless login, proceeding without it."
|
||||
"[login] unable to create URL for seamless login, proceeding without it.",
|
||||
e
|
||||
)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||
* automatically redirecting to it inside of `signIn`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||
*/
|
||||
console.log({ login_NEXTAUTH_URL: process.env.NEXTAUTH_URL })
|
||||
console.log(`[login] final redirectUrl: ${redirectTo}`)
|
||||
console.log({ login_env: process.env })
|
||||
|
||||
console.log({ login_redirectTo: redirectTo })
|
||||
const params = {
|
||||
/** Record<string, any> is next-auth typings */
|
||||
const params: Record<string, any> = {
|
||||
ui_locales: context.params.lang,
|
||||
scope: ["openid", "profile"].join(" "),
|
||||
scope: ["openid", "profile"],
|
||||
/**
|
||||
* The `acr_values` param is used to make Curity display the proper login
|
||||
* page for Scandic. Without the parameter Curity presents some choices
|
||||
@@ -117,18 +135,25 @@ export async function GET(
|
||||
// This is new param set for differentiate between the Magic link login of New web and current web
|
||||
version: "2",
|
||||
}
|
||||
|
||||
if (isMFA) {
|
||||
// Append profile_update scope for MFA
|
||||
params.scope = params.scope + " profile_udpate"
|
||||
params.scope.push("profile_update")
|
||||
/**
|
||||
* The below acr value is required as for New Web same Curity Client is used for MFA
|
||||
* while in current web it is being setup using different Curity Client
|
||||
*/
|
||||
params.acr_values =
|
||||
"urn:se:curity:authentication:otp-authenticator:OTP-Authenticator_web"
|
||||
} else if (isMagicLinkUpdateLogin) {
|
||||
} else if (isSeamlessMagicLink) {
|
||||
params.acr_values = "abc"
|
||||
}
|
||||
params.scope = params.scope.join(" ")
|
||||
/**
|
||||
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||
* automatically redirecting to it inside of `signIn`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||
*/
|
||||
const redirectUrl = await signIn(
|
||||
"curity",
|
||||
{
|
||||
@@ -139,9 +164,13 @@ export async function GET(
|
||||
)
|
||||
|
||||
if (redirectUrl) {
|
||||
return NextResponse.redirect(redirectUrl, {
|
||||
const redirectOpts = {
|
||||
headers: redirectHeaders,
|
||||
})
|
||||
}
|
||||
console.log(`[login] redirecting to: ${redirectUrl}`, redirectOpts)
|
||||
return NextResponse.redirect(redirectUrl, redirectOpts)
|
||||
} else {
|
||||
console.error(`[login] missing redirectUrl reponse from signIn()`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -12,39 +12,54 @@ export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: { lang: Lang } }
|
||||
) {
|
||||
let redirectTo: string
|
||||
|
||||
// Set redirect url from the magicLinkRedirect Cookie which is set when intiating login
|
||||
redirectTo =
|
||||
request.cookies.get("magicLinkRedirectTo")?.value ||
|
||||
"/" + context.params.lang
|
||||
|
||||
if (!env.PUBLIC_URL) {
|
||||
throw internalServerError("No value for env.PUBLIC_URL")
|
||||
}
|
||||
|
||||
const loginKey = request.nextUrl.searchParams.get("loginKey")
|
||||
if (!loginKey) {
|
||||
console.log(
|
||||
`[verifymagiclink] missing required loginKey, aborting bad request`
|
||||
)
|
||||
return badRequest()
|
||||
}
|
||||
|
||||
let redirectTo: string
|
||||
|
||||
console.log(`[verifymagiclink] verifying callback`)
|
||||
|
||||
const redirectToCookieValue = request.cookies.get(
|
||||
"magicLinkRedirectTo"
|
||||
)?.value // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login
|
||||
const redirectToFallback = "/"
|
||||
|
||||
console.log(
|
||||
`[verifymagiclink] magicLinkRedirectTo cookie value: ${redirectToCookieValue}`
|
||||
)
|
||||
|
||||
redirectTo = redirectToCookieValue || redirectToFallback
|
||||
|
||||
// Make relative URL to absolute URL
|
||||
if (redirectTo.startsWith("/")) {
|
||||
console.log(
|
||||
`[verifymagiclink] make redirectTo absolute, from ${redirectTo}`
|
||||
)
|
||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
||||
console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`)
|
||||
}
|
||||
|
||||
// Update Seamless login url as Magic link login has a different authenticator in Curity
|
||||
redirectTo = redirectTo.replace("updatelogin", "updateloginemail")
|
||||
|
||||
const loginKey = request.nextUrl.searchParams.get("loginKey")
|
||||
|
||||
if (!loginKey) {
|
||||
return badRequest()
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[verifymagiclink] final redirectUrl: ${redirectTo}`)
|
||||
|
||||
/**
|
||||
* Passing `redirect: false` to `signIn` will return the URL instead of
|
||||
* automatically redirecting to it inside of `signIn`.
|
||||
* https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76
|
||||
*/
|
||||
console.log({ login_redirectTo: redirectTo })
|
||||
let redirectUrl = await signIn(
|
||||
const redirectUrl = await signIn(
|
||||
"curity",
|
||||
{
|
||||
redirectTo,
|
||||
@@ -61,7 +76,12 @@ export async function GET(
|
||||
)
|
||||
|
||||
if (redirectUrl) {
|
||||
console.log(`[verifymagiclink] redirecting to: ${redirectUrl}`)
|
||||
return NextResponse.redirect(redirectUrl)
|
||||
} else {
|
||||
console.error(
|
||||
`[verifymagiclink] missing redirectUrl reponse from signIn()`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -15,14 +15,9 @@ import { getIntl } from "@/i18n"
|
||||
import ServerIntlProvider from "@/i18n/Provider"
|
||||
import { getLang, setLang } from "@/i18n/serverContext"
|
||||
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "New web",
|
||||
title: "Scandic Hotels",
|
||||
}
|
||||
export { generateMetadata } from "@/utils/generateMetadata"
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
@@ -35,8 +30,8 @@ export default async function RootLayout({
|
||||
>) {
|
||||
setLang(params.lang)
|
||||
preloadUserTracking()
|
||||
|
||||
const { defaultLocale, locale, messages } = await getIntl()
|
||||
|
||||
return (
|
||||
<html lang={getLang()}>
|
||||
<head>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function NotFound({ params }: PageArgs<LangParams>) {
|
||||
setLang(params.lang)
|
||||
export default async function NotFound() {
|
||||
const { formatMessage } = await getIntl()
|
||||
return (
|
||||
<main>
|
||||
|
||||
@@ -21,6 +21,7 @@ export default async function ContentTypePage({
|
||||
const user = await serverClient().user.get()
|
||||
|
||||
if (!user) {
|
||||
console.log(`[webview:page] unable to load user`)
|
||||
return <p>Error: No user could be loaded</p>
|
||||
}
|
||||
|
||||
@@ -31,9 +32,16 @@ export default async function ContentTypePage({
|
||||
case "token_expired":
|
||||
const h = headers()
|
||||
const returnURL = `/${getLang()}/webview${h.get("x-pathname")!}`
|
||||
redirect(
|
||||
`/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}`
|
||||
)
|
||||
const redirectURL = `/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}`
|
||||
console.log(`[webview:page] user error, redirecting to: ${redirectURL}`)
|
||||
redirect(redirectURL)
|
||||
case "notfound":
|
||||
return <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 { Lang } from "@/constants/languages"
|
||||
import { profile } from "@/constants/routes/myPages"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { lang: string } }
|
||||
) {
|
||||
try {
|
||||
const lang = params.lang as Lang
|
||||
console.log(`[add-card] callback started`)
|
||||
const lang = params.lang as Lang
|
||||
const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`)
|
||||
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const success = searchParams.get("success")
|
||||
const failure = searchParams.get("failure")
|
||||
const cancel = searchParams.get("cancel")
|
||||
const trxId = searchParams.get("datatransTrxId")
|
||||
|
||||
const returnUrl = new URL(
|
||||
`${env.PUBLIC_URL}/${lang ?? Lang.en}/scandic-friends/my-pages/profile`
|
||||
)
|
||||
|
||||
if (success) {
|
||||
if (!trxId) {
|
||||
return badRequest("Missing datatransTrxId param")
|
||||
}
|
||||
if (trxId) {
|
||||
const saveCardSuccess = await serverClient().user.creditCard.save({
|
||||
transactionId: trxId,
|
||||
})
|
||||
|
||||
const saveCardSuccess = await serverClient().user.saveCard({
|
||||
transactionId: trxId,
|
||||
})
|
||||
|
||||
if (saveCardSuccess) {
|
||||
returnUrl.searchParams.set("success", "true")
|
||||
if (saveCardSuccess) {
|
||||
console.log(`[add-card] planet success: card saved success`)
|
||||
returnUrl.searchParams.set("success", "true")
|
||||
} else {
|
||||
console.log(`[add-card] planet success: card saved fail`)
|
||||
returnUrl.searchParams.set("failure", "true")
|
||||
}
|
||||
} else {
|
||||
returnUrl.searchParams.set("failure", "true")
|
||||
console.log(`[add-card] planet success: missing datatransTrxId`)
|
||||
returnUrl.searchParams.set("error", "true")
|
||||
}
|
||||
} else if (failure) {
|
||||
console.log(`[add-card] planet fail`)
|
||||
returnUrl.searchParams.set("failure", "true")
|
||||
} else if (cancel) {
|
||||
console.log(`[add-card] planet cancel`)
|
||||
returnUrl.searchParams.set("cancel", "true")
|
||||
}
|
||||
|
||||
return Response.redirect(returnUrl, 307)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return internalServerError()
|
||||
} catch (e) {
|
||||
console.error(`[add-card] error saving credit card`, e)
|
||||
returnUrl.searchParams.set("error", "true")
|
||||
}
|
||||
|
||||
console.log(`[add-card] redirecting to: ${returnUrl}`)
|
||||
return Response.redirect(returnUrl)
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
:root {
|
||||
--max-width: 113.5rem;
|
||||
--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
|
||||
},
|
||||
async redirect({ baseUrl, url }) {
|
||||
console.log(`[auth] deciding redirect URL`, { baseUrl, url })
|
||||
if (url.startsWith("/")) {
|
||||
console.log(`[auth] relative URL accepted, returning: ${baseUrl}${url}`)
|
||||
// Allows relative callback URLs
|
||||
return `${baseUrl}${url}`
|
||||
} else {
|
||||
@@ -146,17 +148,19 @@ export const config = {
|
||||
if (
|
||||
/\.scandichotels\.(dk|de|com|fi|no|se)$/.test(parsedUrl.hostname)
|
||||
) {
|
||||
console.log(`[auth] subdomain URL accepted, returning: ${url}`)
|
||||
// Allows any subdomains on all top level domains above
|
||||
return url
|
||||
} else if (parsedUrl.origin === baseUrl) {
|
||||
// Allows callback URLs on the same origin
|
||||
console.log(`[auth] origin URL accepted, returning: ${url}`)
|
||||
return url
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in auth redirect callback")
|
||||
console.error(e)
|
||||
console.error(`[auth] error parsing incoming URL for redirection`, e)
|
||||
}
|
||||
}
|
||||
console.log(`[auth] URL denied, returning base URL: ${baseUrl}`)
|
||||
return baseUrl
|
||||
},
|
||||
async authorized({ auth, request }) {
|
||||
|
||||
@@ -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 Hero from "@/components/Hero"
|
||||
import Intro from "@/components/Intro"
|
||||
import { Blocks } from "@/components/Loyalty/Blocks"
|
||||
import Sidebar from "@/components/Loyalty/Sidebar"
|
||||
import MaxWidth from "@/components/MaxWidth"
|
||||
import Preamble from "@/components/TempDesignSystem/Text/Preamble"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
|
||||
@@ -16,7 +19,7 @@ export default async function LoyaltyPage() {
|
||||
}
|
||||
|
||||
const { tracking, loyaltyPage } = loyaltyPageRes
|
||||
|
||||
const heroImage = loyaltyPage.heroImage
|
||||
return (
|
||||
<>
|
||||
<section className={styles.content}>
|
||||
@@ -24,8 +27,22 @@ export default async function LoyaltyPage() {
|
||||
<Sidebar blocks={loyaltyPage.sidebar} />
|
||||
) : null}
|
||||
|
||||
<MaxWidth className={styles.blocks} tag="main">
|
||||
<Title>{loyaltyPage.heading}</Title>
|
||||
<MaxWidth className={styles.blocks}>
|
||||
<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}
|
||||
</MaxWidth>
|
||||
</section>
|
||||
@@ -15,6 +15,11 @@
|
||||
padding-right: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.content {
|
||||
gap: var(--Spacing-x5);
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function LoginButton({
|
||||
id={trackingId}
|
||||
color={color}
|
||||
href={`${login[lang]}?redirectTo=${encodeURIComponent(pathName)}`}
|
||||
prefetch={false}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function Header({
|
||||
/**
|
||||
* ToDo: Create logic to get this info from ContentStack based on page
|
||||
* */
|
||||
const hideBookingWidget = false
|
||||
const hideBookingWidget = true
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
|
||||
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 alt = image.meta.alt ?? image.title
|
||||
|
||||
const width = parseInt(attrs.width.replaceAll("px", ""))
|
||||
const width = attrs.width
|
||||
? parseInt(attrs.width.replaceAll("px", ""))
|
||||
: image.dimensions.width
|
||||
const props = extractPossibleAttributes(attrs)
|
||||
return (
|
||||
<section key={node.uid}>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"level": 1,
|
||||
"name": "New Friend",
|
||||
"requirement": "0p",
|
||||
"requirement": "0 Punkte",
|
||||
"description": "Dies ist der Beginn von etwas Wunderbarem: Als New Friend können Sie sich auf eine Reise voller herrlicher Scandic-Entdeckungen freuen.",
|
||||
"icon": "/_static/icons/loyaltylevels/new-friend.svg",
|
||||
"benefits": [
|
||||
@@ -78,7 +78,7 @@
|
||||
{
|
||||
"level": 2,
|
||||
"name": "Good Friend",
|
||||
"requirement": "5 000p",
|
||||
"requirement": "5 000 Punkte",
|
||||
"description": "Sie waren in letzter Zeit viel bei uns! Und ehrlich gesagt haben wir das Gefühl, dass wir auf einer Wellenlänge sind – die vielen angenehmen Aufenthalte und lustigen Überraschungen sprechen für sich.",
|
||||
"icon": "/_static/icons/loyaltylevels/good-friend.svg",
|
||||
"benefits": [
|
||||
@@ -153,7 +153,7 @@
|
||||
{
|
||||
"level": 3,
|
||||
"name": "Close Friend",
|
||||
"requirement": "10 000p",
|
||||
"requirement": "10 000 Punkte",
|
||||
"description": "Jetzt wird es ernst: Wir lernen uns wirklich besser kennen, was bedeutet, dass Ihre Zeit mit Scandic noch viel persönlicher wird.",
|
||||
"icon": "/_static/icons/loyaltylevels/close-friend.svg",
|
||||
"benefits": [
|
||||
@@ -229,7 +229,7 @@
|
||||
{
|
||||
"level": 4,
|
||||
"name": "Dear Friend",
|
||||
"requirement": "25 000p",
|
||||
"requirement": "25 000 Punkte",
|
||||
"description": "Ein Hoch auf uns! Unser Verhältnis scheint sich in Richtung Freunde fürs Leben zu entwickeln – was auch bedeutet, dass Sie Zugang zu einer ganzen Menge mehr Scandic bekommen.",
|
||||
"icon": "/_static/icons/loyaltylevels/dear-friend.svg",
|
||||
"benefits": [
|
||||
@@ -306,7 +306,7 @@
|
||||
{
|
||||
"level": 5,
|
||||
"name": "Loyal Friend",
|
||||
"requirement": "100 000p",
|
||||
"requirement": "100 000 Punkte",
|
||||
"description": "Sie haben uns während zahlreicher Aufenthalte, Happy Hours und Workouts im Fitnessstudio die Treue gehalten – deshalb wollen wir uns mit einigen unserer großartigsten Belohnungen bei Ihnen revanchieren.",
|
||||
"icon": "/_static/icons/loyaltylevels/loyal-friend.svg",
|
||||
"benefits": [
|
||||
@@ -383,7 +383,7 @@
|
||||
{
|
||||
"level": 6,
|
||||
"name": "True Friend",
|
||||
"requirement": "250 000p",
|
||||
"requirement": "250 000 Punkte",
|
||||
"description": "Es spielt keine Rolle, ob Haupt- oder Nebensaison: Sie sind immer für uns da. Genießen Sie noch mehr individuelle Vorteile – genau nach Ihrem Geschmack.",
|
||||
"icon": "/_static/icons/loyaltylevels/true-friend.svg",
|
||||
"benefits": [
|
||||
@@ -460,7 +460,7 @@
|
||||
{
|
||||
"level": 7,
|
||||
"name": "Best Friend",
|
||||
"requirement": "400 000p oder 100 nächte",
|
||||
"requirement": "400 000 Punkte oder 100 Nächte",
|
||||
"description": "Für eine Freundschaft wie diese gibt es im Grunde keine passenden Worte, aber wir versuchen es trotzdem: Denn es könnte gar nichts Besseres geben, wenn es um sehr, sehr exklusive Erlebnisse geht!",
|
||||
"icon": "/_static/icons/loyaltylevels/best-friend.svg",
|
||||
"benefits": [
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
{
|
||||
"level": 1,
|
||||
"name": "New Friend",
|
||||
"requirement": "0p",
|
||||
"description": "Olemme uuden ja upean kynnyksellä: New Friend -ystävänä pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.",
|
||||
"requirement": "0 p",
|
||||
"description": "Ystävänämme pääset nauttimaan kaikesta ihanasta, mitä Scandic tarjoaa.",
|
||||
"icon": "/_static/icons/loyaltylevels/new-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Mikä herkullinen etu! Hyödynnä 10 %:n alennus hotelliemme ravintoloissa ja shopissa viikonloppuisin. Tarjous on voimassa niin majoittujille kuin hotellitunnelmaa hetkeksi etsiville. Hemmottele siis itseäsi ja löydä tie lähimpään Scandiciin.",
|
||||
"unlocked": true,
|
||||
"value": "10%"
|
||||
"value": "10 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
@@ -39,38 +39,38 @@
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
@@ -78,29 +78,29 @@
|
||||
{
|
||||
"level": 2,
|
||||
"name": "Good Friend",
|
||||
"requirement": "5 000p",
|
||||
"description": "Kiva, että olet vieraillut meillä, ja tuntuu, että ystävyytemme on hyvässä nosteessa. Tästä on hyvä jatkaa, yksi yöpyminen ja iloinen yllätys kerrallaan!",
|
||||
"requirement": "5 000 p",
|
||||
"description": "Tästä on hyvä jatkaa, yksi yöpyminen ja iloinen yllätys kerrallaan!",
|
||||
"icon": "/_static/icons/loyaltylevels/good-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!",
|
||||
"description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
@@ -114,38 +114,38 @@
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
@@ -153,29 +153,29 @@
|
||||
{
|
||||
"level": 3,
|
||||
"name": "Close Friend",
|
||||
"requirement": "10 000p",
|
||||
"description": "Onpa kiva, että olet vieraillut meillä näin usein! Nyt etusi vain paranevat, sillä olemmehan jo enemmän kuin hyvän päivän tuttuja.",
|
||||
"requirement": "10 000 p",
|
||||
"description": "Nyt etusi vain paranevat, sillä olemmehan jo enemmän kuin hyvän päivän tuttuja.",
|
||||
"icon": "/_static/icons/loyaltylevels/close-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!",
|
||||
"description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
@@ -190,38 +190,38 @@
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
@@ -229,29 +229,29 @@
|
||||
{
|
||||
"level": 4,
|
||||
"name": "Dear Friend",
|
||||
"requirement": "25 000p",
|
||||
"requirement": "25 000 p",
|
||||
"description": "Kippis syventyvälle ystävyydellemme. Nyt pääset nauttimaan liudasta uusia etuja.",
|
||||
"icon": "/_static/icons/loyaltylevels/dear-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!",
|
||||
"description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
@@ -264,41 +264,41 @@
|
||||
"name": "Enemmän pisteitä",
|
||||
"description": "Tässä lisäboostia sinulle: saat 25 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa.",
|
||||
"unlocked": true,
|
||||
"value": "25%"
|
||||
"value": "25 %"
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
@@ -306,29 +306,29 @@
|
||||
{
|
||||
"level": 5,
|
||||
"name": "Loyal Friend",
|
||||
"requirement": "100 000p",
|
||||
"description": "Kiva, että olemme saaneet jakaa paljon yhteisiä hetkiä. Olet tosiaan nimesi arvoinen Loyal Friend! Haluamme panostaa ystävyyteemme myös jatkossa ja annammekin sinulle kasan uusia, ihania etuja.",
|
||||
"requirement": "100 000 p",
|
||||
"description": "Haluamme panostaa ystävyyteemme myös jatkossa ja annammekin sinulle kasan uusia, ihania etuja.",
|
||||
"icon": "/_static/icons/loyaltylevels/loyal-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!",
|
||||
"description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
@@ -341,41 +341,41 @@
|
||||
"name": "Enemmän pisteitä",
|
||||
"description": "Tässä lisäboostia sinulle: saat 25 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa.",
|
||||
"unlocked": true,
|
||||
"value": "25%"
|
||||
"value": "25 %"
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
@@ -383,29 +383,29 @@
|
||||
{
|
||||
"level": 6,
|
||||
"name": "True Friend",
|
||||
"requirement": "250 000p",
|
||||
"description": "Onpa ollut ihana nähdä sinua näin paljon viime aikoina. Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.",
|
||||
"requirement": "250 000 p",
|
||||
"description": "Tosiystävän tapaan haluamme palkita sinua entistä yksilöllisemmillä eduilla.",
|
||||
"icon": "/_static/icons/loyaltylevels/true-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!",
|
||||
"description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
@@ -416,43 +416,43 @@
|
||||
},
|
||||
{
|
||||
"name": "Enemmän pisteitä",
|
||||
"description": "Tässä extra-boostia sinulle: saat 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa. ",
|
||||
"description": "Saat 25 % tai 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä!.",
|
||||
"unlocked": true,
|
||||
"value": "50%"
|
||||
"value": "50 %"
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": false
|
||||
}
|
||||
]
|
||||
@@ -460,76 +460,76 @@
|
||||
{
|
||||
"level": 7,
|
||||
"name": "Best Friend",
|
||||
"requirement": "400 000p tai 100 yötä",
|
||||
"description": "Ystävyytemme on vailla vertaa. Koska sanat eivät riitä kiittämään ystävyydestämme, pääset nyt käsiksi kaikkein eksklusiivisimpiin elämyksiin.",
|
||||
"requirement": "400 000 p tai 100 yötä",
|
||||
"description": "Koska sanat eivät riitä kiittämään ystävyydestämme, pääset nyt käsiksi kaikkein eksklusiivisimpiin elämyksiin.",
|
||||
"icon": "/_static/icons/loyaltylevels/best-friend.svg",
|
||||
"benefits": [
|
||||
{
|
||||
"name": "Ystävähinnat",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan. Ei salaisia koodeja tai kädenpuristuksia – sen kuin vain hyppäät varaamaan huoletta.",
|
||||
"description": "Ystävänämme saat aina parhaan hinnan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Alennus ruoasta",
|
||||
"description": "Maukas etu sinulle! Ystävänämme saat 10-15% alennusta ruoasta hotelliemme ravintoloissa ja shopissa. Tarjous on voimassa viikonloppuisin ja valittuina loma-aikoina, majoitut meillä tai et. Hemmottele siis itseäsi – olet sen ansainnut!",
|
||||
"description": "Ystävänämme saat 10-15 % alennusta ruoasta hotelliemme ravintoloissa ja shopissa viikonloppuisin ja valittuina loma-aikoina.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Mocktail lapsille yöpymisen yhteydessä maksutta",
|
||||
"description": "Lapset mukana matkassa? Meillä lapset ovat superstaroja ja siksi he saavat raikkaan mocktailin majoittumisen yhteydessä meidän piikkiin.",
|
||||
"name": "Mocktail lapsille maksutta",
|
||||
"description": "Meillä lapset saavat raikkaan mocktailin majoittumisen yhteydessä.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Myöhäinen uloskirjautuminen varaustilanteen mukaan",
|
||||
"description": "Joskus myöhäinen uloskirjautuminen pelastaa päivän. Nyt ystävyteemme on jo sillä tasolla, että sinulla ei ole kiire hypätä sängystä aamuvarhaisella. Kirjaudu ulos tuntia myöhemmin ilman lisämaksua ja ota kaikki irti extra-ajasta.",
|
||||
"name": "Myöhäinen uloskirjautuminen",
|
||||
"description": "Kirjaudu ulos tuntia myöhemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Ravintolakuponki",
|
||||
"description": "Parhaana ystävänämme saat 20 € ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä. Illallinen hotellin ravintolassa tai kasa herkkuja huoneeseen – mihin sinä sen käyttäisit?",
|
||||
"description": "Ystävänämme saat ravintolakupongin jokaisesta pisteisiin oikeuttavasta yöstä.",
|
||||
"unlocked": true,
|
||||
"value": "20 €"
|
||||
},
|
||||
{
|
||||
"name": "Enemmän pisteitä",
|
||||
"description": "Tässä extra-boostia sinulle: saat 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä! Pistä tuulemaan ja haali pisteet yöpymisistä, aterioista ja muusta, niin pääset nauttimaan palkintoyöstä tuossa tuokiossa. ",
|
||||
"description": "Saat 25 % tai 50 % enemmän pisteitä joka kerta kun ansaitset pisteitä!.",
|
||||
"unlocked": true,
|
||||
"value": "50%"
|
||||
"value": "50 %"
|
||||
},
|
||||
{
|
||||
"name": "Aikainen sisäänkirjautuminen varaustilanteen mukaan",
|
||||
"description": "Ota varaslähtö yöpymiseen! Kirjaudu sisään tuntia aiemmin ilman lisämaksua ja ota oikotie rentoutumiseen.",
|
||||
"name": "Aikainen sisäänkirjautuminen",
|
||||
"description": "Kirjaudu sisään tuntia aiemmin ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Maksuton huoneluokan korotus varaustilanteen mukaan",
|
||||
"description": "Ystävyytemme on seuraavalla vaihteella ja nyt saat huoneluokan korotuksen ilman lisämaksua aina kun siihen on mahdollisuus. Luvassa siis entistä mukavampi majoitus!",
|
||||
"name": "Maksuton huoneluokan korotus",
|
||||
"description": "Saat huoneluokan korotuksen ilman lisämaksua. Saatavilla varaustilanteen mukaan.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen – kaksi yhden hinnalla",
|
||||
"description": "Herkkuja kaksin kerroin! Nyt aamiaiselle kannattaa saapua kaverin kanssa, sillä saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et. Valmistaudu aamutreffeihin huolella tutustumalla lisätietoihin.",
|
||||
"description": "Saat kaksi aamiaista yhden hinnalla, majoitut meillä tai et.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "48 tunnin huonetakuu",
|
||||
"description": "Uniikki etu harvoille ja valituille – eli sinulle! Ystävänäsi lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi. Aika taikuruutta, vai mitä?",
|
||||
"description": "Lupaamme sinulle huoneen, vaikka hotelli olisi täyteen buukattu, kunhan varaat sen vähintään 48 tuntia ennen saapumistasi.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Aamiainen aina maksutta",
|
||||
"description": "Haluaisitko aloittaa aamusi herkullisesti? Tarjoamme sinulle valmiin hotelliaamiaisen maksutta vaikka joka aamu. Etu on käytettävissäsi riippumatta siitä, majoitutko meillä vai et.",
|
||||
"description": "Tarjoamme sinulle hotelliaamiaisen maksutta, majoitut meillä tai et.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Upea vuotuinen lahja",
|
||||
"description": "Best Friend -ystävänämme ansaitset kuninkaallista kohtelua. Siksi palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella lahjalla. Mikä se on? No se on tietysti yllätys.",
|
||||
"description": "Palkitsemme sinut vuosittain mahtavalla ja eksklusiivisella yllätyslahjalla.",
|
||||
"unlocked": true
|
||||
},
|
||||
{
|
||||
"name": "Kid’s boost",
|
||||
"description": "Ystävyytemme ulottuu myös mukana matkaaviin lapsiin. Muistamme pikkuväkeä pienellä lahjalla joka kerta kun majoitutte meillä. Pienet VIP-vieraat ovat sen arvoisia, vai mitä!",
|
||||
"description": "Muistamme lapsia pienellä lahjalla majoituksen yhteydessä.",
|
||||
"unlocked": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Nam! Nyt en smakfull 10 % rabatt i restauranten og shoppen vår i helgene. Dette tilbudet gjelder enten du er gjesten vår over natten eller bare kommer innom for en matbit. Så, sett i gang, unn deg selv noe godt.",
|
||||
"unlocked": true,
|
||||
"value": "10%"
|
||||
"value": "10 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -34,7 +34,7 @@
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"name": "Friendsboost",
|
||||
"description": "",
|
||||
"unlocked": false
|
||||
},
|
||||
@@ -89,9 +89,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -109,7 +109,7 @@
|
||||
"unlocked": false
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"name": "Friendsboost",
|
||||
"description": "",
|
||||
"unlocked": false
|
||||
},
|
||||
@@ -164,9 +164,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du bor hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15 % rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du bor hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -185,7 +185,7 @@
|
||||
"value": "50 NOK"
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"name": "Friendsboost",
|
||||
"description": "",
|
||||
"unlocked": false
|
||||
},
|
||||
@@ -240,9 +240,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -261,10 +261,10 @@
|
||||
"value": "75 NOK"
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"name": "Friendsboost",
|
||||
"description": "Her har du noe veldig bra: Hver gang du øker antall vennskapspoeng, får du 25 % ekstra – ekstra på det ekstra! Så, begynn å samle poeng på opphold, måltider og mer, og du vil veldig snart få et gratis opphold.",
|
||||
"unlocked": true,
|
||||
"value": "25%"
|
||||
"value": "25 %"
|
||||
},
|
||||
{
|
||||
"name": "Tidlig innsjekk når tilgjengelig",
|
||||
@@ -317,9 +317,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -338,10 +338,10 @@
|
||||
"value": "100 NOK"
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"name": "Friendsboost",
|
||||
"description": "Her har du noe veldig bra: Hver gang du øker antall vennskapspoeng, får du 25 % ekstra – ekstra på det ekstra! Så, begynn å samle poeng på opphold, måltider og mer, og du vil veldig snart få et gratis opphold.",
|
||||
"unlocked": true,
|
||||
"value": "25%"
|
||||
"value": "25 %"
|
||||
},
|
||||
{
|
||||
"name": "Tidlig innsjekk når tilgjengelig",
|
||||
@@ -394,9 +394,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -415,10 +415,10 @@
|
||||
"value": "150 NOK"
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"description": "Du kan virkelig glede deg. Hver gang du øker antall vennskapspoeng, får du 50 % ekstra – ekstra på det ekstra! Så, få flere poeng på opphold, måltider og mer, og du vil få et gratis opphold lynraskt",
|
||||
"name": "Friendsboost",
|
||||
"description": "Gled deg! Hver gang du tjener nye Friends-poeng får du 25 % eller 50 % ekstra poeng – som en superboost! Begynn å tjene poeng ved å bo og spise hos oss, og du vil få en bonusnatt før du aner det.",
|
||||
"unlocked": true,
|
||||
"value": "50%"
|
||||
"value": "50 %"
|
||||
},
|
||||
{
|
||||
"name": "Tidlig innsjekk når tilgjengelig",
|
||||
@@ -471,9 +471,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Rabatt på mat",
|
||||
"description": "Hva er bedre enn en rabatt? Som vår venn får du 10-15% rabatt på mat i restauranten og shoppen vår i helger og på utvalgte helligdager – og det gjelder både når du har opphold hos oss og når du ikke har det. Så kom igjen, skjem deg selv bort.",
|
||||
"description": "Hva er bedre enn rabatt? Som vår Friend får du 10-15 % rabatt på mat i vår restaurant og shop i helger og utvalgte ferier og helligdager. Det gjelder uansett om du bor hos oss eller ikke. Så kom igjen, skjem deg bort.",
|
||||
"unlocked": true,
|
||||
"value": "15%"
|
||||
"value": "15 %"
|
||||
},
|
||||
{
|
||||
"name": "Gratis barne-mocktail under oppholdet",
|
||||
@@ -492,10 +492,10 @@
|
||||
"value": "200 NOK"
|
||||
},
|
||||
{
|
||||
"name": "Ekstra vennskap",
|
||||
"description": "Du kan virkelig glede deg. Hver gang du øker antall vennskapspoeng, får du 50 % ekstra – ekstra på det ekstra! Så, få flere poeng på opphold, måltider og mer, og du vil få et gratis opphold lynraskt",
|
||||
"name": "Friendsboost",
|
||||
"description": "Gled deg! Hver gang du tjener nye Friends-poeng får du 25 % eller 50 % ekstra poeng – som en superboost! Begynn å tjene poeng ved å bo og spise hos oss, og du vil få en bonusnatt før du aner det.",
|
||||
"unlocked": true,
|
||||
"value": "50%"
|
||||
"value": "50 %"
|
||||
},
|
||||
{
|
||||
"name": "Tidlig innsjekk når tilgjengelig",
|
||||
|
||||
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 SidebarMyPages from "@/components/MyPages/Sidebar"
|
||||
|
||||
import JoinLoyaltyContact from "./JoinLoyalty"
|
||||
import { MyPagesNavigation } from "./MyPagesNavigation"
|
||||
|
||||
import styles from "./sidebar.module.css"
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function SidebarLoyalty({ blocks }: SidebarProps) {
|
||||
case SidebarTypenameEnum.LoyaltyPageSidebarDynamicContent:
|
||||
switch (block.dynamic_content.component) {
|
||||
case LoyaltySidebarDynamicComponentEnum.my_pages_navigation:
|
||||
return <SidebarMyPages key={`${block.__typename}-${idx}`} />
|
||||
return <MyPagesNavigation key={`${block.__typename}-${idx}`} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default async function Friend({
|
||||
{formatMessage(
|
||||
isHighestLevel
|
||||
? { id: "Highest level" }
|
||||
: { id: "Your current level" }
|
||||
: { id: `Level ${membershipLevels[membership.membershipLevel]}` }
|
||||
)}
|
||||
</Body>
|
||||
{membership ? (
|
||||
|
||||
@@ -7,9 +7,8 @@ import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
import DesktopTable from "./Desktop"
|
||||
import MobileTable from "./Mobile"
|
||||
import Pagination from "./Pagination"
|
||||
import Table from "./Table"
|
||||
|
||||
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
@@ -40,8 +39,7 @@ export default function TransactionTable({
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
<MobileTable transactions={data?.data.transactions || []} />
|
||||
<DesktopTable transactions={data?.data.transactions || []} />
|
||||
<Table transactions={data?.data.transactions || []} />
|
||||
{data && data.meta.totalPages > 1 ? (
|
||||
<Pagination
|
||||
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 {
|
||||
border: 1px solid #e6e9ec;
|
||||
border-bottom: 1px solid var(--Scandic-Brand-Pale-Peach);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.td {
|
||||
background-color: #fff;
|
||||
color: var(--UI-Text-High-contrast);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x4);
|
||||
padding: var(--Spacing-x2);
|
||||
position: relative;
|
||||
text-align: left;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
}
|
||||
|
||||
.addition {
|
||||
@@ -17,8 +25,7 @@
|
||||
.addition::before {
|
||||
color: var(--Secondary-Light-On-Surface-Accent);
|
||||
content: "+";
|
||||
left: var(--Spacing-x2);
|
||||
position: absolute;
|
||||
margin-right: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.negation {
|
||||
@@ -28,6 +35,11 @@
|
||||
.negation::before {
|
||||
color: var(--Base-Text-Accent);
|
||||
content: "-";
|
||||
left: var(--Spacing-x2);
|
||||
position: absolute;
|
||||
margin-right: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.td {
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import Row from "./Row"
|
||||
|
||||
import styles from "./desktop.module.css"
|
||||
import styles from "./table.module.css"
|
||||
|
||||
import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
const tableHeadings = [
|
||||
"Arrival date",
|
||||
"Points",
|
||||
"Description",
|
||||
"Booking number",
|
||||
"Transaction date",
|
||||
"Points",
|
||||
"Arrival date",
|
||||
]
|
||||
|
||||
export default function DesktopTable({ transactions }: TableProps) {
|
||||
export default function Table({ transactions }: TableProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{transactions.length ? (
|
||||
<div>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
{tableHeadings.map((heading) => (
|
||||
<th key={heading} className={styles.th}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: heading })}
|
||||
</Body>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction, idx) => (
|
||||
<Row
|
||||
key={`${transaction.confirmationNumber}-${idx}`}
|
||||
transaction={transaction}
|
||||
/>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
{tableHeadings.map((heading) => (
|
||||
<th key={heading} className={styles.th}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: heading })}
|
||||
</Body>
|
||||
</th>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction, index) => (
|
||||
<Row
|
||||
key={`${transaction.confirmationNumber}-${index}`}
|
||||
transaction={transaction}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
@@ -1,5 +1,8 @@
|
||||
.container {
|
||||
display: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: auto;
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.table {
|
||||
@@ -17,7 +20,8 @@
|
||||
|
||||
.th {
|
||||
text-align: left;
|
||||
padding: 20px 32px;
|
||||
text-wrap: nowrap;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
@@ -49,9 +53,10 @@
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default async function Breadcrumbs() {
|
||||
if (!breadcrumbs?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const homeBreadcrumb = breadcrumbs.shift()
|
||||
return (
|
||||
<nav className={styles.breadcrumbs}>
|
||||
|
||||
@@ -1,64 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { PlusCircleIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./addCreditCardButton.module.css"
|
||||
|
||||
import { type AddCreditCardButtonProps } from "@/types/components/myPages/myProfile/addCreditCardButton"
|
||||
|
||||
let hasRunOnce = false
|
||||
|
||||
function useAddCardResultToast() {
|
||||
const hasRunOnce = useRef(false)
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRunOnce) return
|
||||
if (hasRunOnce.current) return
|
||||
|
||||
const success = searchParams.get("success")
|
||||
const failure = searchParams.get("failure")
|
||||
const cancel = searchParams.get("cancel")
|
||||
const error = searchParams.get("error")
|
||||
|
||||
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(
|
||||
intl.formatMessage({ id: "Your card was successfully saved!" })
|
||||
)
|
||||
})
|
||||
} else if (failure) {
|
||||
setTimeout(() => {
|
||||
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
||||
})
|
||||
toast.success(
|
||||
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 || error) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
router.replace(pathname)
|
||||
hasRunOnce = true
|
||||
hasRunOnce.current = true
|
||||
}, [intl, pathname, router, searchParams])
|
||||
}
|
||||
|
||||
export default function AddCreditCardButton({
|
||||
redirectUrl,
|
||||
}: AddCreditCardButtonProps) {
|
||||
export default function AddCreditCardButton() {
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
useAddCardResultToast()
|
||||
|
||||
const initiateAddCard = trpc.user.initiateSaveCard.useMutation({
|
||||
onSuccess: (result) => (result ? router.push(result.attribute.link) : null),
|
||||
onError: () =>
|
||||
toast.error(intl.formatMessage({ id: "Something went wrong!" })),
|
||||
const initiateAddCard = trpc.user.creditCard.add.useMutation({
|
||||
onSuccess: (result) => {
|
||||
if (result?.attribute.link) {
|
||||
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 (
|
||||
@@ -70,8 +87,6 @@ export default function AddCreditCardButton({
|
||||
onClick={() =>
|
||||
initiateAddCard.mutate({
|
||||
language: lang,
|
||||
mobileToken: false,
|
||||
redirectUrl,
|
||||
})
|
||||
}
|
||||
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 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>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
asChild: true
|
||||
}
|
||||
|
||||
export type ButtonProps = ButtonPropsSlot | ButtonPropsRAC
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
|
||||
import { buttonVariants } from "./variants"
|
||||
|
||||
import type { ButtonProps } from "./button"
|
||||
|
||||
export default function Button({
|
||||
asChild = false,
|
||||
theme,
|
||||
className,
|
||||
disabled,
|
||||
intent,
|
||||
size,
|
||||
variant,
|
||||
wrapping,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
export default function Button(props: ButtonProps) {
|
||||
const { className, intent, size, theme, wrapping, variant, ...restProps } =
|
||||
props
|
||||
|
||||
const classNames = buttonVariants({
|
||||
className,
|
||||
intent,
|
||||
@@ -26,5 +19,19 @@ export default function Button({
|
||||
wrapping,
|
||||
variant,
|
||||
})
|
||||
return <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 { ImageVaultAsset } from "@/types/components/imageVaultImage"
|
||||
import { ImageVaultAsset } from "@/types/components/imageVault"
|
||||
|
||||
export interface LoyaltyCardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { toastVariants } from "./variants"
|
||||
import styles from "./toasts.module.css"
|
||||
|
||||
export function ToastHandler() {
|
||||
return <Toaster />
|
||||
return <Toaster position="bottom-right" />
|
||||
}
|
||||
|
||||
function getIcon(variant: ToastsProps["variant"]) {
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
background-color: var(--icon-background-color);
|
||||
padding: var(--Spacing-x2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--icon-background-color);
|
||||
padding: var(--Spacing-x2);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@ export const login = {
|
||||
sv: "/sv/logga-in",
|
||||
}
|
||||
|
||||
/** @type {import('@/types/routes').LangRoute} */
|
||||
export const loginUnLocalized = {
|
||||
da: "/da/login",
|
||||
de: "/de/login",
|
||||
en: "/en/login",
|
||||
fi: "/fi/login",
|
||||
no: "/no/login",
|
||||
sv: "/sv/login",
|
||||
}
|
||||
|
||||
/** @type {import('@/types/routes').LangRoute} */
|
||||
export const logout = {
|
||||
da: "/da/log-ud",
|
||||
@@ -22,6 +32,16 @@ export const logout = {
|
||||
sv: "/sv/logga-ut",
|
||||
}
|
||||
|
||||
/** @type {import('@/types/routes').LangRoute} */
|
||||
export const logoutUnLocalized = {
|
||||
da: "/da/logout",
|
||||
de: "/de/logout",
|
||||
en: "/en/logout",
|
||||
fi: "/fi/logout",
|
||||
no: "/no/logout",
|
||||
sv: "/sv/logout",
|
||||
}
|
||||
|
||||
/** @type {import('@/types/routes').LangRoute} */
|
||||
export const verifymagiclink = {
|
||||
da: "/da/verifymagiclink",
|
||||
@@ -36,4 +56,6 @@ export const handleAuth = [
|
||||
...Object.values(login),
|
||||
...Object.values(logout),
|
||||
...Object.values(verifymagiclink),
|
||||
...Object.values(loginUnLocalized),
|
||||
...Object.values(logoutUnLocalized),
|
||||
]
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter",
|
||||
"Already a friend?": "Allerede en ven?",
|
||||
"Amenities": "Faciliteter",
|
||||
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
|
||||
"Are you sure you want to remove the card ending with": "Er du sikker på, at du vil fjerne kortet, der slutter med",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "fra idag",
|
||||
"As our": "Som vores",
|
||||
@@ -31,6 +33,7 @@
|
||||
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
|
||||
"Country": "Land",
|
||||
"Country code": "Landekode",
|
||||
"Credit card deleted successfully": "Kreditkort blev slettet",
|
||||
"Your current level": "Dit nuværende niveau",
|
||||
"Current password": "Nuværende kodeord",
|
||||
"characters": "tegn",
|
||||
@@ -42,13 +45,24 @@
|
||||
"Edit": "Redigere",
|
||||
"Edit profile": "Rediger profil",
|
||||
"Email": "E-mail",
|
||||
"Extras to your booking": "Ekstra til din booking",
|
||||
"There are no transactions to display": "Der er ingen transaktioner at vise",
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||
"Find booking": "Find booking",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
"Former Scandic Hotel": "Tidligere Scandic Hotel",
|
||||
"From": "Fra",
|
||||
"from your member profile?": "fra din medlemsprofil?",
|
||||
"Get inspired": "Bliv inspireret",
|
||||
"Go back to overview": "Gå tilbage til oversigten",
|
||||
"Level 1": "Niveau 1",
|
||||
"Level 2": "Niveau 2",
|
||||
"Level 3": "Niveau 3",
|
||||
"Level 4": "Niveau 4",
|
||||
"Level 5": "Niveau 5",
|
||||
"Level 6": "Niveau 6",
|
||||
"Level 7": "Niveau 7",
|
||||
"Highest level": "Højeste niveau",
|
||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||
"How it works": "Hvordan det virker",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Next": "Næste",
|
||||
"next level:": "Næste niveau:",
|
||||
"No content published": "Intet indhold offentliggjort",
|
||||
"No, keep card": "Nej, behold kortet",
|
||||
"No transactions available": "Ingen tilgængelige transaktioner",
|
||||
"Not found": "Ikke fundet",
|
||||
"night": "nat",
|
||||
@@ -88,7 +103,9 @@
|
||||
"Phone is required": "Telefonnummer er påkrævet",
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer",
|
||||
"Points": "Point",
|
||||
"Points": "Points",
|
||||
"Points being calculated": "Point udregnes",
|
||||
"Points earned prior to May 1, 2021": "Point optjent før 1. maj 2021",
|
||||
"Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.",
|
||||
"Points needed to level up": "Point nødvendige for at komme i niveau",
|
||||
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
|
||||
@@ -96,10 +113,13 @@
|
||||
"Previous victories": "Tidligere sejre",
|
||||
"Read more": "Læs mere",
|
||||
"Read more about the hotel": "Læs mere om hotellet",
|
||||
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Retype new password": "Gentag den nye adgangskode",
|
||||
"Rooms": "Værelser",
|
||||
"Save": "Gemme",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Select a country": "Vælg et land",
|
||||
"Select country of residence": "Vælg bopælsland",
|
||||
"Select date of birth": "Vælg fødselsdato",
|
||||
@@ -108,7 +128,10 @@
|
||||
"Show more": "Vis mere",
|
||||
"Show all amenities": "Vis alle faciliteter",
|
||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||
"Sign up bonus": "Tilmeldingsbonus",
|
||||
"Something went wrong!": "Noget gik galt!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
||||
"Street": "Gade",
|
||||
"special character": "speciel karakter",
|
||||
"Total Points": "Samlet antal point",
|
||||
@@ -117,17 +140,22 @@
|
||||
"Transactions": "Transaktioner",
|
||||
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
|
||||
"to": "til",
|
||||
"TUI Points": "TUI-point",
|
||||
"User information": "Brugeroplysninger",
|
||||
"uppercase letter": "stort bogstav",
|
||||
"Visiting address": "Besøgsadresse",
|
||||
"We could not add a card right now, please try again later.": "Vi kunne ikke tilføje et kort lige nu. Prøv venligst igen senere.",
|
||||
"Welcome": "Velkommen",
|
||||
"Welcome to": "Velkommen til",
|
||||
"Wellness & Exercise": "Velvære & Motion",
|
||||
"Where should you go next?": "Find inspiration til dit næste ophold",
|
||||
"Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig",
|
||||
"Year": "År",
|
||||
"You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.",
|
||||
"Yes, remove my card": "Ja, fjern mit kort",
|
||||
"You have no previous stays.": "Du har ingen tidligere ophold.",
|
||||
"You have no upcoming stays.": "Du har ingen kommende ophold.",
|
||||
"Your card was successfully removed!": "Dit kort blev fjernet!",
|
||||
"Your card was successfully saved!": "Dit kort blev gemt!",
|
||||
"Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!",
|
||||
"Your level": "Dit niveau",
|
||||
@@ -136,6 +164,19 @@
|
||||
"Hotel facilities": "Hotel faciliteter",
|
||||
"Hotel surroundings": "Hotel omgivelser",
|
||||
"Show map": "Vis kort",
|
||||
"Check in": "Check ind",
|
||||
"Check out": "Check ud",
|
||||
"Summary": "Opsummering",
|
||||
"Thank you": "Tak",
|
||||
"We look forward to your visit!": "Vi ser frem til dit besøg!",
|
||||
"We have sent a detailed confirmation of your booking to your email:": "Vi har sendt en detaljeret bekræftelse af din booking til din email:",
|
||||
"Download the Scandic app": "Download Scandic-appen",
|
||||
"View your booking": "Se din booking",
|
||||
"At latest": "Senest",
|
||||
"Type of room": "Værelsestype",
|
||||
"Type of bed": "Sengtype",
|
||||
"Weekdays": "Hverdage",
|
||||
"Weekends": "Weekender",
|
||||
"Where to": "Hvorhen",
|
||||
"When": "Hvornår",
|
||||
"Rooms & Guests": "Værelser & gæster",
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
"All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet",
|
||||
"Already a friend?": "Sind wir schon Freunde?",
|
||||
"Amenities": "Annehmlichkeiten",
|
||||
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||
"Are you sure you want to remove the card ending with": "Möchten Sie die Karte mit der Endung",
|
||||
"Arrival date": "Ankunftsdatum",
|
||||
"as of today": "Ab heute",
|
||||
"as of today": "Stand heute",
|
||||
"As our": "Als unser",
|
||||
"As our Close Friend": "Als unser enger Freund",
|
||||
"At the hotel": "Im Hotel",
|
||||
@@ -30,6 +32,7 @@
|
||||
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
|
||||
"Country": "Land",
|
||||
"Country code": "Landesvorwahl",
|
||||
"Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht",
|
||||
"Your current level": "Ihr aktuelles Level",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"characters": "figuren",
|
||||
@@ -41,13 +44,24 @@
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit profile": "Profil bearbeiten",
|
||||
"Email": "Email",
|
||||
"Extras to your booking": "Extras zu Ihrer Buchung",
|
||||
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"Find booking": "Buchung finden",
|
||||
"Flexibility": "Flexibilität",
|
||||
"Former Scandic Hotel": "Ehemaliges Scandic Hotel",
|
||||
"From": "Fromm",
|
||||
"from your member profile?": "wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
||||
"Get inspired": "Lassen Sie sich inspieren",
|
||||
"Go back to overview": "Zurück zur Übersicht",
|
||||
"Level 1": "Level 1",
|
||||
"Level 2": "Level 2",
|
||||
"Level 3": "Level 3",
|
||||
"Level 4": "Level 4",
|
||||
"Level 5": "Level 5",
|
||||
"Level 6": "Level 6",
|
||||
"Level 7": "Level 7",
|
||||
"Highest level": "Höchstes Level",
|
||||
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
||||
"How it works": "Wie es funktioniert",
|
||||
@@ -71,6 +85,7 @@
|
||||
"Next": "Nächste",
|
||||
"next level:": "Nächstes Level:",
|
||||
"No content published": "Kein Inhalt veröffentlicht",
|
||||
"No, keep card": "Nein, Karte behalten",
|
||||
"No transactions available": "Keine Transaktionen verfügbar",
|
||||
"Not found": "Nicht gefunden",
|
||||
"night": "nacht",
|
||||
@@ -86,15 +101,20 @@
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein",
|
||||
"Points": "Punkte",
|
||||
"Points being calculated": "Punkte werden berechnet",
|
||||
"Points earned prior to May 1, 2021": "Vor dem 1. Mai 2021 gesammelte Punkte",
|
||||
"Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.",
|
||||
"Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden",
|
||||
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Niveau zu bleiben",
|
||||
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
|
||||
"spendable points expiring by": "Einlösbare punkte verfallen bis zum",
|
||||
"Previous victories": "Bisherige Siege",
|
||||
"Read more": "Mehr lesen",
|
||||
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
|
||||
"Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen",
|
||||
"Retype new password": "Neues Passwort erneut eingeben",
|
||||
"Save": "Speichern",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Select a country": "Wähle ein Land",
|
||||
"Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus",
|
||||
"Select date of birth": "Geburtsdatum auswählen",
|
||||
@@ -103,25 +123,33 @@
|
||||
"Show more": "Mehr anzeigen",
|
||||
"Show all amenities": "Alle Annehmlichkeiten anzeigen",
|
||||
"Skip to main content": "Direkt zum Inhalt",
|
||||
"Sign up bonus": "Anmeldebonus",
|
||||
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
||||
"Street": "Straße",
|
||||
"special character": "sonderzeichen",
|
||||
"Total Points": "Gesamtpunktzahl",
|
||||
"Your points to spend": "Deine Punkte",
|
||||
"Your points to spend": "Meine Punkte",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktionen",
|
||||
"Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)",
|
||||
"to": "zu",
|
||||
"TUI Points": "TUI Punkte",
|
||||
"User information": "Nutzerinformation",
|
||||
"uppercase letter": "großbuchstabe",
|
||||
"Visiting address": "Besuchsadresse",
|
||||
"We could not add a card right now, please try again later.": "Wir konnten momentan keine Karte hinzufügen. Bitte versuchen Sie es später noch einmal.",
|
||||
"Welcome to": "Willkommen zu",
|
||||
"Welcome": "Willkommen",
|
||||
"Where should you go next?": "Wo geht es als Nächstes hin?",
|
||||
"Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?",
|
||||
"Year": "Jahr",
|
||||
"You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.",
|
||||
"Yes, remove my card": "Ja, meine Karte entfernen",
|
||||
"You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.",
|
||||
"You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.",
|
||||
"Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!",
|
||||
"Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!",
|
||||
"Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!",
|
||||
"Your level": "Dein level",
|
||||
@@ -130,6 +158,19 @@
|
||||
"Hotel facilities": "Hotel-Infos",
|
||||
"Hotel surroundings": "Umgebung des Hotels",
|
||||
"Show map": "Karte anzeigen",
|
||||
"Check in": "Einchecken",
|
||||
"Check out": "Auschecken",
|
||||
"Summary": "Zusammenfassung",
|
||||
"Thank you": "Danke",
|
||||
"We look forward to your visit!": "Wir freuen uns auf Ihren Besuch!",
|
||||
"We have sent a detailed confirmation of your booking to your email:": "Wir haben eine detaillierte Bestätigung Ihrer Buchung an Ihre E-Mail gesendet:",
|
||||
"Download the Scandic app": "Laden Sie die Scandic-App herunter",
|
||||
"View your booking": "Ihre Buchung ansehen",
|
||||
"At latest": "Spätestens",
|
||||
"Type of room": "Zimmerart",
|
||||
"Type of bed": "Bettentyp",
|
||||
"Weekdays": "Wochentage",
|
||||
"Weekends": "Wochenenden",
|
||||
"Where to": "Wohin",
|
||||
"When": "Wann",
|
||||
"Rooms & Guests": "Zimmer & Gäste",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "All rooms comes with standard amenities",
|
||||
"Already a friend?": "Already a friend?",
|
||||
"Amenities": "Amenities",
|
||||
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
|
||||
"Are you sure you want to remove the card ending with": "Are you sure you want to remove the card ending with",
|
||||
"Arrival date": "Arrival date",
|
||||
"as of today": "as of today",
|
||||
"As our": "As our",
|
||||
@@ -34,6 +36,7 @@
|
||||
"Your current level": "Your current level",
|
||||
"Current password": "Current password",
|
||||
"characters": "characters",
|
||||
"Credit card deleted successfully": "Credit card deleted successfully",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Day": "Day",
|
||||
"Description": "Description",
|
||||
@@ -45,15 +48,26 @@
|
||||
"Email": "Email",
|
||||
"There are no transactions to display": "There are no transactions to display",
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Extras to your booking": "Extras to your booking",
|
||||
"FAQ": "FAQ",
|
||||
"Find booking": "Find booking",
|
||||
"Former Scandic Hotel": "Former Scandic Hotel",
|
||||
"Flexibility": "Flexibility",
|
||||
"From": "From",
|
||||
"from your member profile?": "from your member profile?",
|
||||
"Get inspired": "Get inspired",
|
||||
"Go back to overview": "Go back to overview",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
"hotelPages.rooms.roomCard.persons": "persons",
|
||||
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
|
||||
"Level 1": "Level 1",
|
||||
"Level 2": "Level 2",
|
||||
"Level 3": "Level 3",
|
||||
"Level 4": "Level 4",
|
||||
"Level 5": "Level 5",
|
||||
"Level 6": "Level 6",
|
||||
"Level 7": "Level 7",
|
||||
"Highest level": "Highest level",
|
||||
"How do you want to sleep?": "How do you want to sleep?",
|
||||
"How it works": "How it works",
|
||||
@@ -78,6 +92,7 @@
|
||||
"Next": "Next",
|
||||
"next level:": "next level:",
|
||||
"No content published": "No content published",
|
||||
"No, keep card": "No, keep card",
|
||||
"No transactions available": "No transactions available",
|
||||
"Not found": "Not found",
|
||||
"night": "night",
|
||||
@@ -94,6 +109,8 @@
|
||||
"Phone number": "Phone number",
|
||||
"Please enter a valid phone number": "Please enter a valid phone number",
|
||||
"Points": "Points",
|
||||
"Points being calculated": "Points being calculated",
|
||||
"Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021",
|
||||
"Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.",
|
||||
"Points needed to level up": "Points needed to level up",
|
||||
"Points needed to stay on level": "Points needed to stay on level",
|
||||
@@ -101,11 +118,14 @@
|
||||
"Previous victories": "Previous victories",
|
||||
"Read more": "Read more",
|
||||
"Read more about the hotel": "Read more about the hotel",
|
||||
"Remove card from member profile": "Remove card from member profile",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Retype new password": "Retype new password",
|
||||
"Rooms": "Rooms",
|
||||
"Save": "Save",
|
||||
"See room details": "See room details",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Select a country": "Select a country",
|
||||
"Select country of residence": "Select country of residence",
|
||||
"Select date of birth": "Select date of birth",
|
||||
@@ -114,26 +134,34 @@
|
||||
"Show more": "Show more",
|
||||
"Show all amenities": "Show all amenities",
|
||||
"Skip to main content": "Skip to main content",
|
||||
"Sign up bonus": "Sign up bonus",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
"Street": "Street",
|
||||
"special character": "special character",
|
||||
"Total Points": "Total Points",
|
||||
"Your points to spend": "Your points to spend",
|
||||
"You canceled adding a new credit card.": "You canceled adding a new credit card.",
|
||||
"Transaction date": "Transaction date",
|
||||
"Transactions": "Transactions",
|
||||
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
|
||||
"to": "to",
|
||||
"TUI Points": "TUI Points",
|
||||
"User information": "User information",
|
||||
"uppercase letter": "uppercase letter",
|
||||
"Welcome": "Welcome",
|
||||
"Visiting address": "Visiting address",
|
||||
"We could not add a card right now, please try again later.": "We could not add a card right now, please try again later.",
|
||||
"Welcome to": "Welcome to",
|
||||
"Wellness & Exercise": "Wellness & Exercise",
|
||||
"Where should you go next?": "Where should you go next?",
|
||||
"Which room class suits you the best?": "Which room class suits you the best?",
|
||||
"Year": "Year",
|
||||
"Yes, remove my card": "Yes, remove my card",
|
||||
"You have no previous stays.": "You have no previous stays.",
|
||||
"You have no upcoming stays.": "You have no upcoming stays.",
|
||||
"Your card was successfully removed!": "Your card was successfully removed!",
|
||||
"Your card was successfully saved!": "Your card was successfully saved!",
|
||||
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
|
||||
"Your level": "Your level",
|
||||
@@ -142,6 +170,19 @@
|
||||
"Hotel facilities": "Hotel facilities",
|
||||
"Hotel surroundings": "Hotel surroundings",
|
||||
"Show map": "Show map",
|
||||
"Check in": "Check in",
|
||||
"Check out": "Check out",
|
||||
"Summary": "Summary",
|
||||
"Thank you": "Thank you",
|
||||
"We look forward to your visit!": "We look forward to your visit!",
|
||||
"We have sent a detailed confirmation of your booking to your email: ": "We have sent a detailed confirmation of your booking to your email: ",
|
||||
"Download the Scandic app": "Download the Scandic app",
|
||||
"View your booking": "View your booking",
|
||||
"At latest": "At latest",
|
||||
"Type of room": "Type of room",
|
||||
"Type of bed": "Type of bed",
|
||||
"Weekdays": "Weekdays",
|
||||
"Weekends": "Weekends",
|
||||
"Where to": "Where to",
|
||||
"When": "When",
|
||||
"Rooms & Guests": "Rooms & Guests",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet",
|
||||
"Already a friend?": "Oletko jo ystävä?",
|
||||
"Amenities": "Mukavuudet",
|
||||
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"Are you sure you want to remove the card ending with": "Haluatko varmasti poistaa kortin, joka päättyy numeroon",
|
||||
"Arrival date": "Saapumispäivä",
|
||||
"as of today": "tästä päivästä lähtien",
|
||||
"As our": "Kuin meidän",
|
||||
@@ -31,6 +33,7 @@
|
||||
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
|
||||
"Country": "Maa",
|
||||
"Country code": "Maatunnus",
|
||||
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
|
||||
"Your current level": "Nykyinen tasosi",
|
||||
"Current password": "Nykyinen salasana",
|
||||
"characters": "hahmoja",
|
||||
@@ -42,13 +45,24 @@
|
||||
"Edit": "Muokata",
|
||||
"Edit profile": "Muokkaa profiilia",
|
||||
"Email": "Sähköposti",
|
||||
"Extras to your booking": "Lisävarusteet varaukseesi",
|
||||
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Flexibility": "Joustavuus",
|
||||
"Former Scandic Hotel": "Entinen Scandic Hotel",
|
||||
"From": "From",
|
||||
"from your member profile?": "jäsenprofiilistasi?",
|
||||
"Get inspired": "Inspiroidu",
|
||||
"Go back to overview": "Palaa yleiskatsaukseen",
|
||||
"Level 1": "Taso 1",
|
||||
"Level 2": "Taso 2",
|
||||
"Level 3": "Taso 3",
|
||||
"Level 4": "Taso 4",
|
||||
"Level 5": "Taso 5",
|
||||
"Level 6": "Taso 6",
|
||||
"Level 7": "Taso 7",
|
||||
"Highest level": "Korkein taso",
|
||||
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
||||
"How it works": "Kuinka se toimii",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Next": "Seuraava",
|
||||
"next level:": "Seuraava taso:",
|
||||
"No content published": "Ei julkaistua sisältöä",
|
||||
"No, keep card": "Ei, pidä kortti",
|
||||
"No transactions available": "Ei tapahtumia saatavilla",
|
||||
"Not found": "Ei löydetty",
|
||||
"night": "yö",
|
||||
@@ -89,6 +104,8 @@
|
||||
"Phone number": "Puhelinnumero",
|
||||
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
|
||||
"Points": "Pistettä",
|
||||
"Points being calculated": "Pisteitä lasketaan",
|
||||
"Points earned prior to May 1, 2021": "Ennen 1. toukokuuta 2021 ansaitut pisteet",
|
||||
"Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.",
|
||||
"Points needed to level up": "Pisteitä tarvitaan tasolle pääsemiseksi",
|
||||
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
|
||||
@@ -96,10 +113,13 @@
|
||||
"Previous victories": "Edelliset voitot",
|
||||
"Read more": "Lue lisää",
|
||||
"Read more about the hotel": "Lue lisää hotellista",
|
||||
"Remove card from member profile": "Poista kortti jäsenprofiilista",
|
||||
"Restaurant & Bar": "Ravintola & Baari",
|
||||
"Retype new password": "Kirjoita uusi salasana uudelleen",
|
||||
"Rooms": "Huoneet",
|
||||
"Save": "Tallentaa",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Select a country": "Valitse maa",
|
||||
"Select country of residence": "Valitse asuinmaa",
|
||||
"Select date of birth": "Valitse syntymäaika",
|
||||
@@ -108,7 +128,10 @@
|
||||
"Show more": "Näytä lisää",
|
||||
"Show all amenities": "Näytä kaikki mukavuudet",
|
||||
"Skip to main content": "Siirry pääsisältöön",
|
||||
"Sign up bonus": "Rekisteröidy bonus",
|
||||
"Something went wrong!": "Jotain meni pieleen!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Street": "Katu",
|
||||
"special character": "erikoishahmo",
|
||||
"Total Points": "Kokonaispisteet",
|
||||
@@ -117,18 +140,23 @@
|
||||
"Transactions": "Tapahtumat",
|
||||
"Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)",
|
||||
"to": "to",
|
||||
"TUI Points": "TUI-pisteet",
|
||||
"User information": "Käyttäjän tiedot",
|
||||
"uppercase letter": "iso kirjain",
|
||||
"Visiting address": "Käyntiosoite",
|
||||
"We could not add a card right now, please try again later.": "Emme voineet lisätä korttia juuri nyt. Yritä myöhemmin uudelleen.",
|
||||
"Welcome": "Tervetuloa",
|
||||
"Welcome to": "Tervetuloa",
|
||||
"Wellness & Exercise": "Hyvinvointi & Liikunta",
|
||||
"Where should you go next?": "Mihin menisit seuraavaksi?",
|
||||
"Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?",
|
||||
"Year": "Vuosi",
|
||||
"Yes, remove my card": "Kyllä, poista korttini",
|
||||
"You have no previous stays.": "Sinulla ei ole aiempaa oleskelua.",
|
||||
"You have no upcoming stays.": "Sinulla ei ole tulevia oleskeluja.",
|
||||
"Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!",
|
||||
"Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!",
|
||||
"You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.",
|
||||
"Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!",
|
||||
"Your level": "Tasosi",
|
||||
"Zip code": "Postinumero",
|
||||
@@ -136,6 +164,19 @@
|
||||
"Hotel facilities": "Hotellin palvelut",
|
||||
"Hotel surroundings": "Hotellin ympäristö",
|
||||
"Show map": "Näytä kartta",
|
||||
"Check in": "Sisäänkirjautuminen",
|
||||
"Check out": "Uloskirjautuminen",
|
||||
"Summary": "Yhteenveto",
|
||||
"Thank you": "Kiitos",
|
||||
"We look forward to your visit!": "Odotamme innolla vierailuasi!",
|
||||
"We have sent a detailed confirmation of your booking to your email:": "Olemme lähettäneet yksityiskohtaisen varausvahvistuksen sähköpostiisi:",
|
||||
"Download the Scandic app": "Lataa Scandic-sovellus",
|
||||
"View your booking": "Näytä varauksesi",
|
||||
"At latest": "Viimeistään",
|
||||
"Type of room": "Huonetyyppi",
|
||||
"Type of bed": "Vuodetyyppi",
|
||||
"Weekdays": "Arkisin",
|
||||
"Weekends": "Viikonloppuisin",
|
||||
"Where to": "Minne",
|
||||
"When": "Kun",
|
||||
"Rooms & Guestss": "Huoneet & Vieraat",
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
"All rooms comes with standard amenities": "Alle rommene har standard fasiliteter",
|
||||
"Already a friend?": "Allerede Friend?",
|
||||
"Amenities": "Fasiliteter",
|
||||
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
|
||||
"Are you sure you want to remove the card ending with": "Er du sikker på at du vil fjerne kortet som slutter på",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "per idag",
|
||||
"As our": "Som vår",
|
||||
"As our Close Friend": "Som vår nære venn",
|
||||
"At the hotel": "På hotellet",
|
||||
"Book": "Bok",
|
||||
"Book": "Bestill",
|
||||
"Booking number": "Bestillingsnummer",
|
||||
"Breakfast": "Frokost",
|
||||
"by": "innen",
|
||||
@@ -34,6 +36,7 @@
|
||||
"Your current level": "Ditt nåværende nivå",
|
||||
"Current password": "Nåværende passord",
|
||||
"characters": "tegn",
|
||||
"Credit card deleted successfully": "Kredittkort slettet",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivelse",
|
||||
@@ -42,13 +45,24 @@
|
||||
"Edit": "Redigere",
|
||||
"Edit profile": "Rediger profil",
|
||||
"Email": "E-post",
|
||||
"Extras to your booking": "Ekstra til din bestilling",
|
||||
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||
"Find booking": "Finn booking",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
"Former Scandic Hotel": "Tidligere Scandic Hotel",
|
||||
"From": "Fra",
|
||||
"from your member profile?": "fra medlemsprofilen din?",
|
||||
"Get inspired": "Bli inspirert",
|
||||
"Go back to overview": "Gå tilbake til oversikten",
|
||||
"Level 1": "Nivå 1",
|
||||
"Level 2": "Nivå 2",
|
||||
"Level 3": "Nivå 3",
|
||||
"Level 4": "Nivå 4",
|
||||
"Level 5": "Nivå 5",
|
||||
"Level 6": "Nivå 6",
|
||||
"Level 7": "Nivå 7",
|
||||
"Highest level": "Høyeste nivå",
|
||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||
"How it works": "Hvordan det fungerer",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Next": "Neste",
|
||||
"next level:": "Neste nivå:",
|
||||
"No content published": "Ingen innhold publisert",
|
||||
"No, keep card": "Nei, behold kortet",
|
||||
"No transactions available": "Ingen transaksjoner tilgjengelig",
|
||||
"Not found": "Ikke funnet",
|
||||
"night": "natt",
|
||||
@@ -89,6 +104,8 @@
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer",
|
||||
"Points": "Poeng",
|
||||
"Points being calculated": "Poeng beregnes",
|
||||
"Points earned prior to May 1, 2021": "Poeng opptjent før 1. mai 2021",
|
||||
"Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.",
|
||||
"Points needed to level up": "Poeng som trengs for å komme opp i nivå",
|
||||
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
|
||||
@@ -96,10 +113,13 @@
|
||||
"Previous victories": "Tidligere seire",
|
||||
"Read more": "Les mer",
|
||||
"Read more about the hotel": "Les mer om hotellet",
|
||||
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Retype new password": "Skriv inn nytt passord på nytt",
|
||||
"Rooms": "Rom",
|
||||
"Save": "Lagre",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Select a country": "Velg et land",
|
||||
"Select country of residence": "Velg bostedsland",
|
||||
"Select date of birth": "Velg fødselsdato",
|
||||
@@ -108,7 +128,10 @@
|
||||
"Show more": "Vis mer",
|
||||
"Show all amenities": "Vis alle fasiliteter",
|
||||
"Skip to main content": "Gå videre til hovedsiden",
|
||||
"Sign up bonus": "Registreringsbonus",
|
||||
"Something went wrong!": "Noe gikk galt!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
||||
"Street": "Gate",
|
||||
"special character": "spesiell karakter",
|
||||
"Total Points": "Totale poeng",
|
||||
@@ -117,17 +140,22 @@
|
||||
"Transactions": "Transaksjoner",
|
||||
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
|
||||
"to": "til",
|
||||
"TUI Points": "TUI-poeng",
|
||||
"User information": "Brukerinformasjon",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Visiting address": "Besøksadresse",
|
||||
"We could not add a card right now, please try again later.": "Vi kunne ikke legge til et kort akkurat nå. Prøv igjen senere.",
|
||||
"Welcome": "Velkommen",
|
||||
"Welcome to": "Velkommen til",
|
||||
"Wellness & Exercise": "Velvære & Trening",
|
||||
"Where should you go next?": "Hvor ønsker du å reise neste gang?",
|
||||
"Which room class suits you the best?": "Hvilken romklasse passer deg best?",
|
||||
"Year": "År",
|
||||
"You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.",
|
||||
"Yes, remove my card": "Ja, fjern kortet mitt",
|
||||
"You have no previous stays.": "Du har ingen tidligere opphold.",
|
||||
"You have no upcoming stays.": "Du har ingen kommende opphold.",
|
||||
"Your card was successfully removed!": "Kortet ditt ble fjernet!",
|
||||
"Your card was successfully saved!": "Kortet ditt ble lagret!",
|
||||
"Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!",
|
||||
"Your level": "Ditt nivå",
|
||||
@@ -136,6 +164,19 @@
|
||||
"Hotel facilities": "Hotelfaciliteter",
|
||||
"Hotel surroundings": "Hotellomgivelser",
|
||||
"Show map": "Vis kart",
|
||||
"Check in": "Sjekk inn",
|
||||
"Check out": "Sjekk ut",
|
||||
"Summary": "Sammendrag",
|
||||
"Thank you": "Takk",
|
||||
"We look forward to your visit!": "Vi ser frem til ditt besøk!",
|
||||
"We have sent a detailed confirmation of your booking to your email:": "Vi har sendt en detaljert bekreftelse av din bestilling til din e-post:",
|
||||
"Download the Scandic app": "Last ned Scandic-appen",
|
||||
"View your booking": "Se din bestilling",
|
||||
"At latest": "Senest",
|
||||
"Type of room": "Romtype",
|
||||
"Type of bed": "Sengtype",
|
||||
"Weekdays": "Hverdager",
|
||||
"Weekends": "Helger",
|
||||
"Where to": "Hvor skal du",
|
||||
"When": "Når",
|
||||
"Rooms & Guests": "Rom og gjester",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Alla rum har standardbekvämligheter",
|
||||
"Already a friend?": "Är du redan en vän?",
|
||||
"Amenities": "Bekvämligheter",
|
||||
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
|
||||
"Are you sure you want to remove the card ending with": "Är du säker på att du vill ta bort kortet som slutar med",
|
||||
"Arrival date": "Ankomstdatum",
|
||||
"as of today": "från och med idag",
|
||||
"As our": "Som vår",
|
||||
@@ -31,6 +33,7 @@
|
||||
"Could not find requested resource": "Det gick inte att hitta den begärda resursen",
|
||||
"Country": "Land",
|
||||
"Country code": "Landskod",
|
||||
"Credit card deleted successfully": "Kreditkort har tagits bort",
|
||||
"Your current level": "Din nuvarande nivå",
|
||||
"Current password": "Nuvarande lösenord",
|
||||
"characters": "tecken",
|
||||
@@ -42,13 +45,24 @@
|
||||
"Edit": "Redigera",
|
||||
"Edit profile": "Redigera profil",
|
||||
"Email": "E-post",
|
||||
"Extras to your booking": "Extra till din bokning",
|
||||
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Flexibility": "Flexibilitet",
|
||||
"Former Scandic Hotel": "Tidigare Scandic Hotel",
|
||||
"From": "Från",
|
||||
"from your member profile?": "från din medlemsprofil?",
|
||||
"Get inspired": "Bli inspirerad",
|
||||
"Go back to overview": "Gå tillbaka till översikten",
|
||||
"Level 1": "Nivå 1",
|
||||
"Level 2": "Nivå 2",
|
||||
"Level 3": "Nivå 3",
|
||||
"Level 4": "Nivå 4",
|
||||
"Level 5": "Nivå 5",
|
||||
"Level 6": "Nivå 6",
|
||||
"Level 7": "Nivå 7",
|
||||
"Highest level": "Högsta nivå",
|
||||
"How do you want to sleep?": "Hur vill du sova?",
|
||||
"How it works": "Hur det fungerar",
|
||||
@@ -76,6 +90,7 @@
|
||||
"Next": "Nästa",
|
||||
"next level:": "Nästa nivå:",
|
||||
"No content published": "Inget innehåll publicerat",
|
||||
"No, keep card": "Nej, behåll kortet",
|
||||
"No transactions available": "Inga transaktioner tillgängliga",
|
||||
"Not found": "Hittades inte",
|
||||
"night": "natt",
|
||||
@@ -92,6 +107,8 @@
|
||||
"Phone number": "Telefonnummer",
|
||||
"Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer",
|
||||
"Points": "Poäng",
|
||||
"Points being calculated": "Poäng beräknas",
|
||||
"Points earned prior to May 1, 2021": "Poäng intjänade före 1 maj 2021",
|
||||
"Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.",
|
||||
"Points needed to level up": "Poäng som behövs för att gå upp i nivå",
|
||||
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
|
||||
@@ -99,10 +116,13 @@
|
||||
"Previous victories": "Tidigare segrar",
|
||||
"Read more": "Läs mer",
|
||||
"Read more about the hotel": "Läs mer om hotellet",
|
||||
"Remove card from member profile": "Ta bort kortet från medlemsprofilen",
|
||||
"Restaurant & Bar": "Restaurang & Bar",
|
||||
"Retype new password": "Upprepa nytt lösenord",
|
||||
"Rooms": "Rum",
|
||||
"Save": "Spara",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||
"Select a country": "Välj ett land",
|
||||
"Select country of residence": "Välj bosättningsland",
|
||||
"Select date of birth": "Välj födelsedatum",
|
||||
@@ -111,7 +131,10 @@
|
||||
"Show more": "Visa mer",
|
||||
"Show all amenities": "Visa alla bekvämligheter",
|
||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||
"Sign up bonus": "Registreringsbonus",
|
||||
"Something went wrong!": "Något gick fel!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
||||
"Street": "Gata",
|
||||
"special character": "speciell karaktär",
|
||||
"Total Points": "Poäng totalt",
|
||||
@@ -120,16 +143,21 @@
|
||||
"Transactions": "Transaktioner",
|
||||
"Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)",
|
||||
"to": "till",
|
||||
"TUI Points": "TUI-poäng",
|
||||
"User information": "Användar information",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Visiting address": "Besöksadress",
|
||||
"We could not add a card right now, please try again later.": "Vi kunde inte lägga till ett kort just nu, vänligen försök igen senare.",
|
||||
"Welcome": "Välkommen",
|
||||
"Wellness & Exercise": "Hälsa & Träning",
|
||||
"Where should you go next?": "Låter inte en spontanweekend härligt?",
|
||||
"Which room class suits you the best?": "Vilken rumsklass passar dig bäst?",
|
||||
"Year": "År",
|
||||
"You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.",
|
||||
"Yes, remove my card": "Ja, ta bort mitt kort",
|
||||
"You have no previous stays.": "Du har inga tidigare vistelser.",
|
||||
"You have no upcoming stays.": "Du har inga planerade resor.",
|
||||
"Your card was successfully removed!": "Ditt kort har tagits bort!",
|
||||
"Your card was successfully saved!": "Ditt kort har sparats!",
|
||||
"Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!",
|
||||
"Your level": "Din nivå",
|
||||
@@ -138,6 +166,19 @@
|
||||
"Hotel facilities": "Hotellfaciliteter",
|
||||
"Hotel surroundings": "Hotellomgivning",
|
||||
"Show map": "Visa karta",
|
||||
"Check in": "Checka in",
|
||||
"Check out": "Checka ut",
|
||||
"Summary": "Sammanfattning",
|
||||
"Thank you": "Tack",
|
||||
"We look forward to your visit!": "Vi ser fram emot ditt besök!",
|
||||
"We have sent a detailed confirmation of your booking to your email:": "Vi har skickat en detaljerad bekräftelse av din bokning till din e-post:",
|
||||
"Download the Scandic app": "Ladda ner Scandic-appen",
|
||||
"View your booking": "Visa din bokning",
|
||||
"At latest": "Senast",
|
||||
"Type of room": "Rumstyp",
|
||||
"Type of bed": "Sängtyp",
|
||||
"Weekdays": "Vardagar",
|
||||
"Weekends": "Helger",
|
||||
"Where to": "Vart",
|
||||
"When": "När",
|
||||
"Rooms & Guests": "Rum och gäster",
|
||||
|
||||
@@ -13,6 +13,8 @@ export namespace endpoints {
|
||||
upcomingStays = "booking/v1/Stays/future",
|
||||
previousStays = "booking/v1/Stays/past",
|
||||
hotels = "hotel/v1/Hotels",
|
||||
intiateSaveCard = `${creditCards}/initiateSaveCard`,
|
||||
deleteCreditCard = `${profile}/creditCards`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const fetch = fetchRetry(global.fetch, {
|
||||
})
|
||||
|
||||
export async function get(
|
||||
endpoint: Endpoint | `${endpoints.v1.hotels}/${string}`,
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params?: URLSearchParams
|
||||
) {
|
||||
@@ -38,7 +38,7 @@ export async function get(
|
||||
}
|
||||
|
||||
export async function patch(
|
||||
endpoint: Endpoint,
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithJSONBody
|
||||
) {
|
||||
const { body, ...requestOptions } = options
|
||||
@@ -54,11 +54,12 @@ export async function patch(
|
||||
|
||||
export async function post(
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithJSONBody
|
||||
options: RequestOptionsWithJSONBody,
|
||||
params?: URLSearchParams
|
||||
) {
|
||||
const { body, ...requestOptions } = options
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}`,
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
|
||||
merge.all([
|
||||
defaultOptions,
|
||||
{ body: JSON.stringify(body), method: "POST" },
|
||||
@@ -68,11 +69,12 @@ export async function post(
|
||||
}
|
||||
|
||||
export async function remove(
|
||||
endpoint: Endpoint,
|
||||
options: RequestOptionsWithOutBody
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params?: URLSearchParams
|
||||
) {
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}`,
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
|
||||
merge.all([defaultOptions, { method: "DELETE" }, options])
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
system {
|
||||
@@ -3,7 +3,7 @@
|
||||
#import "../Fragments/MyPages/AccountPage/AccountPageContentTextContent.graphql"
|
||||
|
||||
#import "../Fragments/Refs/MyPages/AccountPage.graphql"
|
||||
#import "../Fragments/Refs/ContentPage.graphql"
|
||||
#import "../Fragments/Refs/ContentPage/ContentPage.graphql"
|
||||
#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql"
|
||||
#import "../Fragments/Refs/System.graphql"
|
||||
|
||||
|
||||
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/LoyaltyCard.graphql"
|
||||
|
||||
#import "../Fragments/LoyaltyPage/Breadcrumbs.graphql"
|
||||
#import "../Fragments/PageLink/AccountPageLink.graphql"
|
||||
#import "../Fragments/PageLink/ContentPageLink.graphql"
|
||||
#import "../Fragments/PageLink/LoyaltyPageLink.graphql"
|
||||
|
||||
#import "../Fragments/Refs/MyPages/AccountPage.graphql"
|
||||
#import "../Fragments/Refs/ContentPage.graphql"
|
||||
#import "../Fragments/Refs/ContentPage/ContentPage.graphql"
|
||||
#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql"
|
||||
#import "../Fragments/Refs/System.graphql"
|
||||
|
||||
@@ -107,6 +106,8 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
|
||||
}
|
||||
title
|
||||
heading
|
||||
preamble
|
||||
hero_image
|
||||
sidebar {
|
||||
__typename
|
||||
... on LoyaltyPageSidebarDynamicContent {
|
||||
@@ -167,7 +168,6 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
...LoyaltyPageBreadcrumbs
|
||||
system {
|
||||
uid
|
||||
created_at
|
||||
|
||||
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/LoyaltyPageLink.graphql"
|
||||
#import "../Fragments/Refs/MyPages/AccountPage.graphql"
|
||||
#import "../Fragments/Refs/ContentPage.graphql"
|
||||
#import "../Fragments/Refs/ContentPage/ContentPage.graphql"
|
||||
#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql"
|
||||
#import "../Fragments/Refs/System.graphql"
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ export async function request<T>(
|
||||
const nr = Math.random()
|
||||
console.log(`START REQUEST ${nr}`)
|
||||
console.time(`OUTGOING REQUEST ${nr}`)
|
||||
console.log(`Sending reqeust to ${env.CMS_URL}`)
|
||||
console.log(`Query:`, print(query as DocumentNode))
|
||||
console.log(`Variables:`, variables)
|
||||
// console.log(`Sending reqeust to ${env.CMS_URL}`)
|
||||
// console.log(`Query:`, print(query as DocumentNode))
|
||||
// console.log(`Variables:`, variables)
|
||||
|
||||
const response = await client.request<T>({
|
||||
document: query,
|
||||
@@ -64,7 +64,7 @@ export async function request<T>(
|
||||
})
|
||||
|
||||
console.timeEnd(`OUTGOING REQUEST ${nr}`)
|
||||
console.log({ response })
|
||||
// console.log({ response })
|
||||
|
||||
return { data: response }
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createTRPCReact } from "@trpc/react-query"
|
||||
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"
|
||||
|
||||
import type { AppRouter } from "@/server"
|
||||
|
||||
export const trpc = createTRPCReact<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 { Lang } from "@/constants/languages"
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
import { webviews } from "@/constants/routes/webviews"
|
||||
import { appRouter } from "@/server"
|
||||
import { createContext } from "@/server/context"
|
||||
@@ -13,12 +14,11 @@ const createCaller = createCallerFactory(appRouter)
|
||||
export function serverClient() {
|
||||
return createCaller(createContext(), {
|
||||
onError: ({ ctx, error, input, path, type }) => {
|
||||
console.error(`Server Client error for ${type}: ${path}`)
|
||||
console.error(`[serverClient] error for ${type}: ${path}`, error)
|
||||
|
||||
if (input) {
|
||||
console.error(`Received input:`)
|
||||
console.error(input)
|
||||
console.error(`[serverClient] received input:`, input)
|
||||
}
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof TRPCError) {
|
||||
if (error.code === "UNAUTHORIZED") {
|
||||
@@ -41,12 +41,13 @@ export function serverClient() {
|
||||
redirectUrl
|
||||
)
|
||||
|
||||
console.log(`[serverClient] onError redirecting to: ${redirectUrl}`)
|
||||
redirect(redirectUrl)
|
||||
}
|
||||
|
||||
redirect(
|
||||
`/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
|
||||
)
|
||||
const redirectUrl = `${login[lang]}?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
|
||||
console.log(`[serverClient] onError redirecting to: ${redirectUrl}`)
|
||||
redirect(redirectUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,9 +49,9 @@ export const middleware = auth(async (request) => {
|
||||
}
|
||||
|
||||
const publicUrl = new URL(env.PUBLIC_URL)
|
||||
const nextUrlClone = nextUrl.clone()
|
||||
nextUrlClone.host = publicUrl.host
|
||||
nextUrlClone.hostname = publicUrl.hostname
|
||||
const nextUrlPublic = nextUrl.clone()
|
||||
nextUrlPublic.host = publicUrl.host
|
||||
nextUrlPublic.hostname = publicUrl.hostname
|
||||
|
||||
/**
|
||||
* Function to validate MFA from token data
|
||||
@@ -67,8 +67,8 @@ export const middleware = auth(async (request) => {
|
||||
|
||||
if (isLoggedIn && isMFAPath && isMFAInvalid()) {
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set("x-mfa-login", "true")
|
||||
headers.set("x-returnurl", nextUrlClone.href)
|
||||
headers.set("x-returnurl", nextUrlPublic.href)
|
||||
headers.set("x-login-source", "mfa")
|
||||
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
|
||||
request: {
|
||||
headers,
|
||||
@@ -87,13 +87,16 @@ export const middleware = auth(async (request) => {
|
||||
const headers = new Headers()
|
||||
headers.append(
|
||||
"set-cookie",
|
||||
`redirectTo=${encodeURIComponent(nextUrlClone.href)}; Path=/; HttpOnly; SameSite=Lax`
|
||||
`redirectTo=${encodeURIComponent(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax`
|
||||
)
|
||||
|
||||
const loginUrl = login[lang]
|
||||
return NextResponse.redirect(new URL(loginUrl, nextUrlClone), {
|
||||
const redirectUrl = new URL(loginUrl, nextUrlPublic)
|
||||
const redirectOpts = {
|
||||
headers,
|
||||
})
|
||||
}
|
||||
console.log(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts)
|
||||
return NextResponse.redirect(redirectUrl, redirectOpts)
|
||||
}) as NextMiddleware // See comment above
|
||||
|
||||
export const matcher: MiddlewareMatcher = (request) => {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const middleware: NextMiddleware = (request) => {
|
||||
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set("x-returnurl", returnUrl)
|
||||
headers.set("x-login-source", "seamless")
|
||||
|
||||
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
|
||||
request: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const middleware: NextMiddleware = (request) => {
|
||||
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set("x-returnurl", returnUrl)
|
||||
headers.set("x-magic-link", "1")
|
||||
headers.set("x-login-source", "seamless-magiclink")
|
||||
|
||||
return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), {
|
||||
request: {
|
||||
|
||||
@@ -23,6 +23,7 @@ export const middleware: NextMiddleware = (request) => {
|
||||
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set("x-returnurl", redirectTo)
|
||||
headers.set("x-logout-source", "seamless")
|
||||
|
||||
return NextResponse.rewrite(new URL(`/${lang}/logout`, request.nextUrl), {
|
||||
request: {
|
||||
|
||||
@@ -34,7 +34,9 @@ export const middleware: NextMiddleware = async (request) => {
|
||||
nextUrlClone.hostname = publicUrl.hostname
|
||||
|
||||
const overviewUrl = overview[lang]
|
||||
return NextResponse.redirect(new URL(overviewUrl, nextUrlClone))
|
||||
const redirectUrl = new URL(overviewUrl, nextUrlClone)
|
||||
console.log(`[myPages] redirecting to: ${redirectUrl}`)
|
||||
return NextResponse.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { env } from "@/env/server"
|
||||
import { internalServerError } from "@/server/errors/next"
|
||||
|
||||
import { findLang } from "@/utils/languages"
|
||||
import { removeTrailingSlash } from "@/utils/url"
|
||||
|
||||
@@ -6,6 +9,17 @@ import type { NextRequest } from "next/server"
|
||||
export function getDefaultRequestHeaders(request: NextRequest) {
|
||||
const lang = findLang(request.nextUrl.pathname)!
|
||||
|
||||
let nextUrl
|
||||
if (env.PUBLIC_URL) {
|
||||
const publicUrl = new URL(env.PUBLIC_URL)
|
||||
const nextUrlPublic = request.nextUrl.clone()
|
||||
nextUrlPublic.host = publicUrl.host
|
||||
nextUrlPublic.hostname = publicUrl.hostname
|
||||
nextUrl = nextUrlPublic
|
||||
} else {
|
||||
nextUrl = request.nextUrl
|
||||
}
|
||||
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set("x-lang", lang)
|
||||
headers.set(
|
||||
@@ -14,7 +28,7 @@ export function getDefaultRequestHeaders(request: NextRequest) {
|
||||
request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "")
|
||||
)
|
||||
)
|
||||
headers.set("x-url", removeTrailingSlash(request.nextUrl.href))
|
||||
headers.set("x-url", removeTrailingSlash(nextUrl.href))
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user