Merge branch 'develop' into feat/sw-222-staycard-link
This commit is contained in:
88
actions/registerUser.ts
Normal file
88
actions/registerUser.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { redirect } from "next/navigation"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { signupVerify } from "@/constants/routes/signup"
|
||||||
|
import * as api from "@/lib/api"
|
||||||
|
import { serviceServerActionProcedure } from "@/server/trpc"
|
||||||
|
|
||||||
|
import { registerSchema } from "@/components/Forms/Register/schema"
|
||||||
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
|
const registerUserPayload = z.object({
|
||||||
|
language: z.string(),
|
||||||
|
firstName: z.string(),
|
||||||
|
lastName: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
phoneNumber: phoneValidator("Phone is required"),
|
||||||
|
dateOfBirth: z.string(),
|
||||||
|
address: z.object({
|
||||||
|
city: z.string().default(""),
|
||||||
|
country: z.string().default(""),
|
||||||
|
countryCode: z.string().default(""),
|
||||||
|
zipCode: z.string().default(""),
|
||||||
|
streetAddress: z.string().default(""),
|
||||||
|
}),
|
||||||
|
password: passwordValidator("Password is required"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerUser = serviceServerActionProcedure
|
||||||
|
.input(registerSchema)
|
||||||
|
.mutation(async function ({ ctx, input }) {
|
||||||
|
const payload = {
|
||||||
|
...input,
|
||||||
|
language: ctx.lang,
|
||||||
|
phoneNumber: input.phoneNumber.replace(/\s+/g, ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = registerUserPayload.safeParse(payload)
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
console.error(
|
||||||
|
"registerUser payload validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: input,
|
||||||
|
error: parsedPayload.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return { success: false, error: "Validation error" }
|
||||||
|
}
|
||||||
|
|
||||||
|
let apiResponse
|
||||||
|
try {
|
||||||
|
apiResponse = await api.post(api.endpoints.v1.profile, {
|
||||||
|
body: parsedPayload.data,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unexpected error", error)
|
||||||
|
return { success: false, error: "Unexpected error" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
console.error(
|
||||||
|
"registerUser api error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: input,
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
error: text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return { success: false, error: "API error" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await apiResponse.json()
|
||||||
|
console.log("registerUser: json", json)
|
||||||
|
|
||||||
|
// Note: The redirect needs to be called after the try/catch block.
|
||||||
|
// See: https://nextjs.org/docs/app/api-reference/functions/redirect
|
||||||
|
redirect(signupVerify[ctx.lang])
|
||||||
|
})
|
||||||
@@ -5,6 +5,5 @@
|
|||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: var(--max-width);
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,12 @@
|
|||||||
grid-template-columns: 1fr 340px;
|
grid-template-columns: 1fr 340px;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
margin: var(--Spacing-x5) auto 0;
|
margin: var(--Spacing-x5) auto 0;
|
||||||
max-width: var(--max-width-navigation);
|
padding-top: var(--Spacing-x6);
|
||||||
padding: var(--Spacing-x6) var(--Spacing-x2) 0;
|
/* simulates padding on viewport smaller than --max-width-navigation */
|
||||||
|
width: min(
|
||||||
|
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||||
|
var(--max-width-navigation)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { redirect } from "next/navigation"
|
|||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
|
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
||||||
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
||||||
import Summary from "@/components/HotelReservation/SelectRate/Summary"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ export default async function StepLayout({
|
|||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
|
setLang(params.lang)
|
||||||
const hotel = await serverClient().hotel.hotelData.get({
|
const hotel = await serverClient().hotel.hotelData.get({
|
||||||
hotelId: "811",
|
hotelId: "811",
|
||||||
language: params.lang,
|
language: params.lang,
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export default function StepPage({
|
|||||||
language: params.lang,
|
language: params.lang,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: userData } = trpc.user.getSafely.useQuery()
|
||||||
|
|
||||||
if (loadingHotel) {
|
if (loadingHotel) {
|
||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
@@ -70,6 +72,11 @@ export default function StepPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let user = null
|
||||||
|
if (userData && !("error" in userData)) {
|
||||||
|
user = userData
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
@@ -97,7 +104,7 @@ export default function StepPage({
|
|||||||
label={intl.formatMessage({ id: "Enter your details" })}
|
label={intl.formatMessage({ id: "Enter your details" })}
|
||||||
path="/details"
|
path="/details"
|
||||||
>
|
>
|
||||||
<Details user={null} />
|
<Details user={user} />
|
||||||
</SectionAccordion>
|
</SectionAccordion>
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header="Payment"
|
header="Payment"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
|
import { overview } from "@/constants/routes/myPages"
|
||||||
|
|
||||||
|
import { auth } from "@/auth"
|
||||||
|
import LoginButton from "@/components/Current/Header/LoginButton"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./signUpVerification.module.css"
|
||||||
|
|
||||||
|
import type { SignUpVerificationProps } from "@/types/components/blocks/dynamicContent"
|
||||||
|
|
||||||
|
export default async function SignUpVerification({
|
||||||
|
dynamic_content,
|
||||||
|
}: SignUpVerificationProps) {
|
||||||
|
const session = await auth()
|
||||||
|
if (session) {
|
||||||
|
redirect(overview[getLang()])
|
||||||
|
}
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<LoginButton
|
||||||
|
className={styles.loginButton}
|
||||||
|
trackingId="signUpVerificationLogin"
|
||||||
|
position="sign up verification"
|
||||||
|
variant="signupVerification"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Proceed to login" })}
|
||||||
|
</LoginButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: var(--Spacing-x3);
|
||||||
|
}
|
||||||
20
components/Blocks/DynamicContent/SignupFormWrapper/index.tsx
Normal file
20
components/Blocks/DynamicContent/SignupFormWrapper/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
|
import { overview } from "@/constants/routes/myPages"
|
||||||
|
|
||||||
|
import { auth } from "@/auth"
|
||||||
|
import Form from "@/components/Forms/Register"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
|
||||||
|
|
||||||
|
export default async function SignupFormWrapper({
|
||||||
|
dynamic_content,
|
||||||
|
}: SignupFormWrapperProps) {
|
||||||
|
const session = await auth()
|
||||||
|
if (session) {
|
||||||
|
// We don't want to allow users to access signup if they are already authenticated.
|
||||||
|
redirect(overview[getLang()])
|
||||||
|
}
|
||||||
|
return <Form {...dynamic_content} />
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPo
|
|||||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel"
|
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel"
|
||||||
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
||||||
|
import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper"
|
||||||
|
import SignUpVerification from "@/components/Blocks/DynamicContent/SignUpVerification"
|
||||||
import PreviousStays from "@/components/Blocks/DynamicContent/Stays/Previous"
|
import PreviousStays from "@/components/Blocks/DynamicContent/Stays/Previous"
|
||||||
import SoonestStays from "@/components/Blocks/DynamicContent/Stays/Soonest"
|
import SoonestStays from "@/components/Blocks/DynamicContent/Stays/Soonest"
|
||||||
import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming"
|
import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming"
|
||||||
@@ -51,6 +53,10 @@ export default async function DynamicContent({
|
|||||||
return <PointsOverview {...dynamic_content} />
|
return <PointsOverview {...dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.previous_stays:
|
case DynamicContentEnum.Blocks.components.previous_stays:
|
||||||
return <PreviousStays {...dynamic_content} />
|
return <PreviousStays {...dynamic_content} />
|
||||||
|
case DynamicContentEnum.Blocks.components.sign_up_form:
|
||||||
|
return <SignupFormWrapper dynamic_content={dynamic_content} />
|
||||||
|
case DynamicContentEnum.Blocks.components.sign_up_verification:
|
||||||
|
return <SignUpVerification dynamic_content={dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.soonest_stays:
|
case DynamicContentEnum.Blocks.components.soonest_stays:
|
||||||
return <SoonestStays {...dynamic_content} />
|
return <SoonestStays {...dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.upcoming_stays:
|
case DynamicContentEnum.Blocks.components.upcoming_stays:
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ export default function LoginButton({
|
|||||||
trackingId,
|
trackingId,
|
||||||
children,
|
children,
|
||||||
color = "black",
|
color = "black",
|
||||||
|
variant = "default",
|
||||||
}: PropsWithChildren<{
|
}: PropsWithChildren<{
|
||||||
className: string
|
className: string
|
||||||
trackingId: string
|
trackingId: string
|
||||||
position: TrackingPosition
|
position: TrackingPosition
|
||||||
color?: LinkProps["color"]
|
color?: LinkProps["color"]
|
||||||
|
variant?: "default" | "signupVerification"
|
||||||
}>) {
|
}>) {
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const pathName = useLazyPathname()
|
const pathName = useLazyPathname()
|
||||||
@@ -49,6 +51,7 @@ export default function LoginButton({
|
|||||||
color={color}
|
color={color}
|
||||||
href={href}
|
href={href}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
variant={variant}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { dt } from "@/lib/dt"
|
|||||||
import DatePicker from "@/components/DatePicker"
|
import DatePicker from "@/components/DatePicker"
|
||||||
import { SearchIcon } from "@/components/Icons"
|
import { SearchIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import Input from "./Input"
|
import Input from "./Input"
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: var(--max-width-navigation);
|
width: min(
|
||||||
width: 100%;
|
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||||
|
var(--max-width-navigation)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
@@ -32,7 +34,8 @@
|
|||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2)
|
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2)
|
||||||
var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.full {
|
.full {
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x5);
|
padding: var(--Spacing-x1) 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export default function Form({ user }: EditFormProps) {
|
|||||||
retypeNewPassword: "",
|
retypeNewPassword: "",
|
||||||
},
|
},
|
||||||
mode: "all",
|
mode: "all",
|
||||||
|
criteriaMode: "all",
|
||||||
resolver: zodResolver(editProfileSchema),
|
resolver: zodResolver(editProfileSchema),
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Key } from "@/components/TempDesignSystem/Form/NewPassword/newPassword"
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
import { phoneValidator } from "@/utils/phoneValidator"
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
const countryRequiredMsg = "Country is required"
|
const countryRequiredMsg = "Country is required"
|
||||||
@@ -26,7 +26,7 @@ export const editProfileSchema = z
|
|||||||
),
|
),
|
||||||
|
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
newPassword: z.string().optional(),
|
newPassword: passwordValidator(),
|
||||||
retypeNewPassword: z.string().optional(),
|
retypeNewPassword: z.string().optional(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
@@ -55,29 +55,6 @@ export const editProfileSchema = z
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.newPassword) {
|
|
||||||
const msgs = []
|
|
||||||
if (data.newPassword.length < 10 || data.newPassword.length > 40) {
|
|
||||||
msgs.push(Key.CHAR_LENGTH)
|
|
||||||
}
|
|
||||||
if (!data.newPassword.match(/[A-Z]/g)) {
|
|
||||||
msgs.push(Key.UPPERCASE)
|
|
||||||
}
|
|
||||||
if (!data.newPassword.match(/[0-9]/g)) {
|
|
||||||
msgs.push(Key.NUM)
|
|
||||||
}
|
|
||||||
if (!data.newPassword.match(/[^A-Za-z0-9]/g)) {
|
|
||||||
msgs.push(Key.SPECIAL_CHAR)
|
|
||||||
}
|
|
||||||
if (msgs.length) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
message: msgs.join(","),
|
|
||||||
path: ["newPassword"],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.newPassword && !data.retypeNewPassword) {
|
if (data.newPassword && !data.retypeNewPassword) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
|
|||||||
49
components/Forms/Register/form.module.css
Normal file
49
components/Forms/Register/form.module.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x5);
|
||||||
|
grid-area: form;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
grid-area: title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userInfo,
|
||||||
|
.password,
|
||||||
|
.terms {
|
||||||
|
align-self: flex-start;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameInputs {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateField {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.formWrapper {
|
||||||
|
gap: var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameInputs {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
185
components/Forms/Register/index.tsx
Normal file
185
components/Forms/Register/index.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { signupTerms } from "@/constants/routes/signup"
|
||||||
|
|
||||||
|
import { registerUser } from "@/actions/registerUser"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
|
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||||
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
|
import NewPassword from "@/components/TempDesignSystem/Form/NewPassword"
|
||||||
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { RegisterSchema, registerSchema } from "./schema"
|
||||||
|
|
||||||
|
import styles from "./form.module.css"
|
||||||
|
|
||||||
|
import type { RegisterFormProps } from "@/types/components/form/registerForm"
|
||||||
|
|
||||||
|
export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const methods = useForm<RegisterSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
dateOfBirth: "",
|
||||||
|
address: {
|
||||||
|
countryCode: "",
|
||||||
|
zipCode: "",
|
||||||
|
},
|
||||||
|
password: "",
|
||||||
|
termsAccepted: false,
|
||||||
|
},
|
||||||
|
mode: "all",
|
||||||
|
criteriaMode: "all",
|
||||||
|
resolver: zodResolver(registerSchema),
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
})
|
||||||
|
const country = intl.formatMessage({ id: "Country" })
|
||||||
|
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
|
||||||
|
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||||
|
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||||
|
|
||||||
|
async function handleSubmit(data: RegisterSchema) {
|
||||||
|
try {
|
||||||
|
const result = await registerUser(data)
|
||||||
|
if (result && !result.success) {
|
||||||
|
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// The server-side redirect will throw an error, which we can ignore
|
||||||
|
// as it's handled by Next.js.
|
||||||
|
if (error instanceof Error && error.message.includes("NEXT_REDIRECT")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.formWrapper}>
|
||||||
|
<Title as="h3">{title}</Title>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
className={styles.form}
|
||||||
|
id="register"
|
||||||
|
/**
|
||||||
|
* Ignoring since ts doesn't recognize that tRPC
|
||||||
|
* parses FormData before reaching the route
|
||||||
|
* @ts-ignore */
|
||||||
|
action={registerUser}
|
||||||
|
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||||
|
>
|
||||||
|
<section className={styles.userInfo}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<header>
|
||||||
|
<Subtitle type="two">
|
||||||
|
{intl.formatMessage({ id: "Contact information" })}
|
||||||
|
</Subtitle>
|
||||||
|
</header>
|
||||||
|
<div className={styles.nameInputs}>
|
||||||
|
<Input
|
||||||
|
label={"firstName"}
|
||||||
|
name="firstName"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={"lastName"}
|
||||||
|
name="lastName"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.dateField}>
|
||||||
|
<header>
|
||||||
|
<Caption textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "Birth date" })}
|
||||||
|
</Caption>
|
||||||
|
</header>
|
||||||
|
<DateSelect
|
||||||
|
name="dateOfBirth"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Input
|
||||||
|
label={zipCode}
|
||||||
|
name="address.zipCode"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<CountrySelect
|
||||||
|
label={country}
|
||||||
|
name="address.countryCode"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label={email}
|
||||||
|
name="email"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<Phone label={phoneNumber} name="phoneNumber" />
|
||||||
|
</section>
|
||||||
|
<section className={styles.password}>
|
||||||
|
<header>
|
||||||
|
<Subtitle type="two">
|
||||||
|
{intl.formatMessage({ id: "Password" })}
|
||||||
|
</Subtitle>
|
||||||
|
</header>
|
||||||
|
<NewPassword
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
label={intl.formatMessage({ id: "Password" })}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section className={styles.terms}>
|
||||||
|
<header>
|
||||||
|
<Subtitle type="two">
|
||||||
|
{intl.formatMessage({ id: "Terms and conditions" })}
|
||||||
|
</Subtitle>
|
||||||
|
</header>
|
||||||
|
<Checkbox name="termsAccepted" registerOptions={{ required: true }}>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
|
||||||
|
})}{" "}
|
||||||
|
<Link
|
||||||
|
variant="underscored"
|
||||||
|
color="peach80"
|
||||||
|
target="_blank"
|
||||||
|
href={signupTerms[lang]}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Scandic's Privacy Policy." })}
|
||||||
|
</Link>
|
||||||
|
</Body>
|
||||||
|
</Checkbox>
|
||||||
|
</section>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
intent="primary"
|
||||||
|
disabled={methods.formState.isSubmitting}
|
||||||
|
data-testid="submit"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Sign up to Scandic Friends" })}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
components/Forms/Register/schema.ts
Normal file
35
components/Forms/Register/schema.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { passwordValidator } from "@/utils/passwordValidator"
|
||||||
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
|
export const registerSchema = z.object({
|
||||||
|
firstName: z
|
||||||
|
.string()
|
||||||
|
.max(250)
|
||||||
|
.refine((value) => value.trim().length > 0, {
|
||||||
|
message: "First name is required",
|
||||||
|
}),
|
||||||
|
lastName: z
|
||||||
|
.string()
|
||||||
|
.max(250)
|
||||||
|
.refine((value) => value.trim().length > 0, {
|
||||||
|
message: "Last name is required",
|
||||||
|
}),
|
||||||
|
email: z.string().max(250).email(),
|
||||||
|
phoneNumber: phoneValidator(
|
||||||
|
"Phone is required",
|
||||||
|
"Please enter a valid phone number"
|
||||||
|
),
|
||||||
|
dateOfBirth: z.string().min(1),
|
||||||
|
address: z.object({
|
||||||
|
countryCode: z.string(),
|
||||||
|
zipCode: z.string().min(1),
|
||||||
|
}),
|
||||||
|
password: passwordValidator("Password is required"),
|
||||||
|
termsAccepted: z.boolean().refine((value) => value === true, {
|
||||||
|
message: "You must accept the terms and conditions",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RegisterSchema = z.infer<typeof registerSchema>
|
||||||
165
components/HotelReservation/EnterDetails/Summary/index.tsx
Normal file
165
components/HotelReservation/EnterDetails/Summary/index.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import { ArrowRightIcon, ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./summary.module.css"
|
||||||
|
|
||||||
|
// TEMP
|
||||||
|
const rooms = [
|
||||||
|
{
|
||||||
|
adults: 1,
|
||||||
|
type: "Cozy cabin",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default async function Summary() {
|
||||||
|
const intl = await getIntl()
|
||||||
|
const lang = getLang()
|
||||||
|
|
||||||
|
const fromDate = dt().locale(lang).format("ddd, D MMM")
|
||||||
|
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM")
|
||||||
|
const diff = dt(toDate).diff(fromDate, "days")
|
||||||
|
|
||||||
|
const totalAdults = rooms.reduce((total, room) => total + room.adults, 0)
|
||||||
|
|
||||||
|
const adults = intl.formatMessage(
|
||||||
|
{ id: "booking.adults" },
|
||||||
|
{ totalAdults: totalAdults }
|
||||||
|
)
|
||||||
|
const nights = intl.formatMessage(
|
||||||
|
{ id: "booking.nights" },
|
||||||
|
{ totalNights: diff }
|
||||||
|
)
|
||||||
|
|
||||||
|
const addOns = [
|
||||||
|
{
|
||||||
|
price: intl.formatMessage({ id: "Included" }),
|
||||||
|
title: intl.formatMessage({ id: "King bed" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: intl.formatMessage({ id: "Included" }),
|
||||||
|
title: intl.formatMessage({ id: "Breakfast buffet" }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const mappedRooms = Array.from(
|
||||||
|
rooms
|
||||||
|
.reduce((acc, room) => {
|
||||||
|
const currentRoom = acc.get(room.type)
|
||||||
|
acc.set(room.type, {
|
||||||
|
total: currentRoom ? currentRoom.total + 1 : 1,
|
||||||
|
type: room.type,
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
}, new Map())
|
||||||
|
.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.summary}>
|
||||||
|
<header>
|
||||||
|
<Body textTransform="bold">
|
||||||
|
{mappedRooms.map(
|
||||||
|
(room, idx) =>
|
||||||
|
`${room.total} x ${room.type}${mappedRooms.length > 1 && idx + 1 !== mappedRooms.length ? ", " : ""}`
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
<Body className={styles.date} color="textMediumContrast">
|
||||||
|
{fromDate}
|
||||||
|
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
|
||||||
|
{toDate}
|
||||||
|
</Body>
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
color="baseButtonTextOnFillNormal"
|
||||||
|
href="#"
|
||||||
|
variant="icon"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "See room details" })}
|
||||||
|
<ChevronRightSmallIcon
|
||||||
|
color="baseButtonTextOnFillNormal"
|
||||||
|
height={20}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
|
<div className={styles.addOns}>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{`${nights}, ${adults}`}
|
||||||
|
</Caption>
|
||||||
|
<Caption color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "4536", currency: "SEK" }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
{addOns.map((addOn) => (
|
||||||
|
<div className={styles.entry} key={addOn.title}>
|
||||||
|
<Caption color="uiTextMediumContrast">{addOn.title}</Caption>
|
||||||
|
<Caption color="uiTextHighContrast">{addOn.price}</Caption>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
|
<div className={styles.total}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "Total incl VAT" })}
|
||||||
|
</Body>
|
||||||
|
<Body textTransform="bold">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "4686", currency: "SEK" }
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Approx." })}
|
||||||
|
</Caption>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "455", currency: "EUR" }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="red" textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "Member price" })}
|
||||||
|
</Body>
|
||||||
|
<Body color="red" textTransform="bold">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "4219", currency: "SEK" }
|
||||||
|
)}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Approx." })}
|
||||||
|
</Caption>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "412", currency: "EUR" }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.summary {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
margin-top: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addOns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
@@ -36,15 +36,18 @@
|
|||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.hotelSelectionHeader {
|
.hotelSelectionHeader {
|
||||||
padding: var(--Spacing-x4) var(--Spacing-x5);
|
padding: var(--Spacing-x4) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelSelectionHeaderWrapper {
|
.hotelSelectionHeaderWrapper {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--Spacing-x6);
|
gap: var(--Spacing-x6);
|
||||||
max-width: var(--max-width-navigation);
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
/* simulates padding on viewport smaller than --max-width-navigation */
|
||||||
|
width: min(
|
||||||
|
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||||
|
var(--max-width-navigation)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.titleContainer > h1 {
|
.titleContainer > h1 {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import styles from "./summary.module.css"
|
|
||||||
|
|
||||||
export default function Summary() {
|
|
||||||
return <div className={styles.wrapper}>Summary TBI</div>
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
}
|
|
||||||
36
components/Icons/EyeHide.tsx
Normal file
36
components/Icons/EyeHide.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { iconVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function EyeHideIcon({ className, color, ...props }: IconProps) {
|
||||||
|
const classNames = iconVariants({ className, color })
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="mask0_69_3263"
|
||||||
|
style={{ maskType: "alpha" }}
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
>
|
||||||
|
<rect width="24" height="24" fill="#D9D9D9" />
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_69_3263)">
|
||||||
|
<path
|
||||||
|
d="M15.9625 13.3L14.6 11.9375C14.7834 11.0335 14.5521 10.2616 13.9063 9.62196C13.2604 8.98231 12.4792 8.74165 11.5625 8.89999L10.2 7.53749C10.4834 7.40832 10.775 7.31145 11.075 7.24686C11.375 7.18228 11.6834 7.14999 12 7.14999C13.2084 7.14999 14.2354 7.5729 15.0813 8.41874C15.9271 9.26457 16.35 10.2917 16.35 11.5C16.35 11.8167 16.3177 12.125 16.2531 12.425C16.1886 12.725 16.0917 13.0167 15.9625 13.3ZM19.05 16.3625L17.7125 15.05C18.3485 14.5667 18.9176 14.0292 19.4197 13.4375C19.9218 12.8458 20.3403 12.2 20.675 11.5C19.8584 9.83332 18.6709 8.51249 17.1125 7.53749C15.5542 6.56249 13.85 6.07499 12 6.07499C11.5167 6.07499 11.0396 6.1104 10.5688 6.18124C10.0979 6.25207 9.62919 6.35832 9.16252 6.49999L7.70002 5.03749C8.39169 4.75415 9.09591 4.54374 9.81267 4.40624C10.5294 4.26874 11.2585 4.19999 12 4.19999C14.2167 4.19999 16.25 4.78749 18.1 5.96249C19.95 7.13749 21.3625 8.71665 22.3375 10.7C22.4042 10.825 22.4521 10.9542 22.4813 11.0877C22.5104 11.2212 22.525 11.3586 22.525 11.5C22.525 11.6413 22.5125 11.7788 22.4875 11.9122C22.4625 12.0457 22.4209 12.1792 22.3625 12.3125C21.9875 13.1208 21.5188 13.8646 20.9563 14.5437C20.3938 15.2229 19.7584 15.8292 19.05 16.3625ZM12 18.8C9.81669 18.8 7.82502 18.2083 6.02502 17.025C4.22502 15.8417 2.79942 14.2901 1.74822 12.3702C1.66609 12.2401 1.60836 12.1013 1.57502 11.9539C1.54169 11.8064 1.52502 11.6552 1.52502 11.5C1.52502 11.3423 1.54077 11.1926 1.57225 11.0507C1.60373 10.9088 1.65882 10.7669 1.73752 10.625C2.07919 9.96665 2.46877 9.33332 2.90627 8.72499C3.34377 8.11665 3.84169 7.56665 4.40002 7.07499L2.32502 4.92499C2.15002 4.74054 2.06669 4.52151 2.07502 4.26791C2.08336 4.0143 2.17502 3.79999 2.35002 3.62499C2.52502 3.44999 2.74169 3.36249 3.00002 3.36249C3.25836 3.36249 3.47502 3.44999 3.65002 3.62499L20.3375 20.3125C20.5209 20.4958 20.6125 20.7167 20.6125 20.975C20.6125 21.2333 20.5167 21.4542 20.325 21.6375C20.1417 21.8125 19.9229 21.8979 19.6688 21.8937C19.4146 21.8896 19.2 21.8 19.025 21.625L15.5875 18.2375C15.0042 18.4208 14.4139 18.5604 13.8166 18.6562C13.2194 18.7521 12.6139 18.8 12 18.8ZM5.72502 8.37499C5.22502 8.80832 4.77086 9.2854 4.36252 9.80624C3.95419 10.3271 3.60836 10.8917 3.32502 11.5C4.14169 13.1667 5.32919 14.4875 6.88752 15.4625C8.44586 16.4375 10.15 16.925 12 16.925C12.3535 16.925 12.6981 16.9042 13.0339 16.8625C13.3696 16.8208 13.7084 16.7667 14.05 16.7L13.1 15.7C12.9167 15.75 12.7363 15.7875 12.5589 15.8125C12.3815 15.8375 12.1952 15.85 12 15.85C10.7917 15.85 9.76461 15.4271 8.91877 14.5812C8.07294 13.7354 7.65002 12.7083 7.65002 11.5C7.65002 11.3167 7.66252 11.1375 7.68752 10.9625C7.71252 10.7875 7.75002 10.6083 7.80002 10.425L5.72502 8.37499Z"
|
||||||
|
fill="#26201E"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
components/Icons/EyeShow.tsx
Normal file
36
components/Icons/EyeShow.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { iconVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function EyeShowIcon({ className, color, ...props }: IconProps) {
|
||||||
|
const classNames = iconVariants({ className, color })
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={classNames}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="mask0_69_3264"
|
||||||
|
style={{ maskType: "alpha" }}
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
>
|
||||||
|
<rect width="24" height="24" fill="#D9D9D9" />
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_69_3264)">
|
||||||
|
<path
|
||||||
|
d="M12 15.85C13.2083 15.85 14.2354 15.4271 15.0812 14.5813C15.9271 13.7354 16.35 12.7083 16.35 11.5C16.35 10.2917 15.9271 9.2646 15.0812 8.41876C14.2354 7.57293 13.2083 7.15001 12 7.15001C10.7916 7.15001 9.76456 7.57293 8.91873 8.41876C8.07289 9.2646 7.64998 10.2917 7.64998 11.5C7.64998 12.7083 8.07289 13.7354 8.91873 14.5813C9.76456 15.4271 10.7916 15.85 12 15.85ZM12.0029 14.15C11.2676 14.15 10.6416 13.8927 10.125 13.378C9.60831 12.8632 9.34998 12.2382 9.34998 11.503C9.34998 10.7677 9.60733 10.1417 10.122 9.62501C10.6367 9.10835 11.2617 8.85001 11.997 8.85001C12.7323 8.85001 13.3583 9.10736 13.875 9.62206C14.3916 10.1368 14.65 10.7618 14.65 11.4971C14.65 12.2324 14.3926 12.8583 13.8779 13.375C13.3632 13.8917 12.7382 14.15 12.0029 14.15ZM12.0019 18.8C9.8256 18.8 7.83956 18.2125 6.04373 17.0375C4.24789 15.8625 2.82498 14.3167 1.77498 12.4C1.69164 12.2583 1.63123 12.1124 1.59373 11.9622C1.55623 11.812 1.53748 11.6578 1.53748 11.4997C1.53748 11.3416 1.55623 11.1875 1.59373 11.0375C1.63123 10.8875 1.69164 10.7417 1.77498 10.6C2.82498 8.68335 4.24727 7.13751 6.04185 5.96251C7.83645 4.78751 9.82187 4.20001 11.9981 4.20001C14.1744 4.20001 16.1604 4.78751 17.9562 5.96251C19.7521 7.13751 21.175 8.68335 22.225 10.6C22.3083 10.7417 22.3687 10.8876 22.4062 11.0378C22.4437 11.1881 22.4625 11.3422 22.4625 11.5003C22.4625 11.6585 22.4437 11.8125 22.4062 11.9625C22.3687 12.1125 22.3083 12.2583 22.225 12.4C21.175 14.3167 19.7527 15.8625 17.9581 17.0375C16.1635 18.2125 14.1781 18.8 12.0019 18.8ZM11.9999 16.925C13.8583 16.925 15.5646 16.4375 17.1187 15.4625C18.6729 14.4875 19.8583 13.1667 20.675 11.5C19.8583 9.83335 18.6729 8.51251 17.1188 7.53751C15.5647 6.56251 13.8584 6.07501 12 6.07501C10.1417 6.07501 8.43539 6.56251 6.88123 7.53751C5.32706 8.51251 4.14164 9.83335 3.32498 11.5C4.14164 13.1667 5.32704 14.4875 6.88118 15.4625C8.43529 16.4375 10.1415 16.925 11.9999 16.925Z"
|
||||||
|
fill="#26201E"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ import {
|
|||||||
DoorOpenIcon,
|
DoorOpenIcon,
|
||||||
ElectricBikeIcon,
|
ElectricBikeIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
|
EyeHideIcon,
|
||||||
|
EyeShowIcon,
|
||||||
FitnessIcon,
|
FitnessIcon,
|
||||||
GiftIcon,
|
GiftIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
@@ -114,6 +116,10 @@ export function getIconByIconName(icon?: IconName): FC<IconProps> | null {
|
|||||||
return ElectricBikeIcon
|
return ElectricBikeIcon
|
||||||
case IconName.Email:
|
case IconName.Email:
|
||||||
return EmailIcon
|
return EmailIcon
|
||||||
|
case IconName.EyeHide:
|
||||||
|
return EyeHideIcon
|
||||||
|
case IconName.EyeShow:
|
||||||
|
return EyeShowIcon
|
||||||
case IconName.Facebook:
|
case IconName.Facebook:
|
||||||
return FacebookIcon
|
return FacebookIcon
|
||||||
case IconName.Fitness:
|
case IconName.Fitness:
|
||||||
|
|||||||
@@ -71,3 +71,8 @@
|
|||||||
.baseButtonTertiaryOnFillNormal * {
|
.baseButtonTertiaryOnFillNormal * {
|
||||||
fill: var(--Base-Button-Tertiary-On-Fill-Normal);
|
fill: var(--Base-Button-Tertiary-On-Fill-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.baseButtonTextOnFillNormal,
|
||||||
|
.baseButtonTextOnFillNormal * {
|
||||||
|
fill: var(--Base-Button-Text-On-Fill-Normal);
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export { default as EditIcon } from "./Edit"
|
|||||||
export { default as ElectricBikeIcon } from "./ElectricBike"
|
export { default as ElectricBikeIcon } from "./ElectricBike"
|
||||||
export { default as EmailIcon } from "./Email"
|
export { default as EmailIcon } from "./Email"
|
||||||
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
export { default as ErrorCircleIcon } from "./ErrorCircle"
|
||||||
|
export { default as EyeHideIcon } from "./EyeHide"
|
||||||
|
export { default as EyeShowIcon } from "./EyeShow"
|
||||||
export { default as FitnessIcon } from "./Fitness"
|
export { default as FitnessIcon } from "./Fitness"
|
||||||
export { default as GiftIcon } from "./Gift"
|
export { default as GiftIcon } from "./Gift"
|
||||||
export { default as GlobeIcon } from "./Globe"
|
export { default as GlobeIcon } from "./Globe"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const config = {
|
|||||||
variants: {
|
variants: {
|
||||||
color: {
|
color: {
|
||||||
baseButtonTertiaryOnFillNormal: styles.baseButtonTertiaryOnFillNormal,
|
baseButtonTertiaryOnFillNormal: styles.baseButtonTertiaryOnFillNormal,
|
||||||
|
baseButtonTextOnFillNormal: styles.baseButtonTextOnFillNormal,
|
||||||
baseIconLowContrast: styles.baseIconLowContrast,
|
baseIconLowContrast: styles.baseIconLowContrast,
|
||||||
black: styles.black,
|
black: styles.black,
|
||||||
blue: styles.blue,
|
blue: styles.blue,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container[data-selected] .checkbox {
|
||||||
|
border: none;
|
||||||
|
background: var(--UI-Input-Controls-Fill-Selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
border: 2px solid var(--UI-Input-Controls-Border-Normal);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 200ms;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 200ms;
|
||||||
|
forced-color-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Scandic-Red-60);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
margin: var(--Spacing-x1) 0 0;
|
||||||
|
}
|
||||||
7
components/TempDesignSystem/Form/Checkbox/checkbox.ts
Normal file
7
components/TempDesignSystem/Form/Checkbox/checkbox.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { RegisterOptions } from "react-hook-form"
|
||||||
|
|
||||||
|
export interface CheckboxProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
name: string
|
||||||
|
registerOptions?: RegisterOptions
|
||||||
|
}
|
||||||
51
components/TempDesignSystem/Form/Checkbox/index.tsx
Normal file
51
components/TempDesignSystem/Form/Checkbox/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Checkbox as AriaCheckbox } from "react-aria-components"
|
||||||
|
import { useController, useFormContext } from "react-hook-form"
|
||||||
|
|
||||||
|
import { InfoCircleIcon } from "@/components/Icons"
|
||||||
|
import CheckIcon from "@/components/Icons/Check"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import { CheckboxProps } from "./checkbox"
|
||||||
|
|
||||||
|
import styles from "./checkbox.module.css"
|
||||||
|
|
||||||
|
export default function Checkbox({
|
||||||
|
name,
|
||||||
|
children,
|
||||||
|
registerOptions,
|
||||||
|
}: React.PropsWithChildren<CheckboxProps>) {
|
||||||
|
const { control } = useFormContext()
|
||||||
|
const { field, fieldState } = useController({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
rules: registerOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaCheckbox
|
||||||
|
className={styles.container}
|
||||||
|
isSelected={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
data-testid={name}
|
||||||
|
>
|
||||||
|
{({ isSelected }) => (
|
||||||
|
<>
|
||||||
|
<div className={styles.checkboxContainer}>
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
{isSelected && <CheckIcon color="white" />}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{fieldState.error ? (
|
||||||
|
<Caption className={styles.error} fontOnly>
|
||||||
|
<InfoCircleIcon color="red" />
|
||||||
|
{fieldState.error.message}
|
||||||
|
</Caption>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AriaCheckbox>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ export default function CountrySelect({
|
|||||||
onSelectionChange={handleChange}
|
onSelectionChange={handleChange}
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
selectedKey={field.value}
|
selectedKey={field.value}
|
||||||
|
data-testid={name}
|
||||||
>
|
>
|
||||||
<div className={styles.comboBoxContainer}>
|
<div className={styles.comboBoxContainer}>
|
||||||
<Label
|
<Label
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { parseDate } from "@internationalized/date"
|
import { parseDate } from "@internationalized/date"
|
||||||
|
import { useState } from "react"
|
||||||
import { DateInput, DatePicker, Group } from "react-aria-components"
|
import { DateInput, DatePicker, Group } from "react-aria-components"
|
||||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
@@ -18,7 +19,7 @@ import type { Key } from "react-aria-components"
|
|||||||
import type { DateProps } from "./date"
|
import type { DateProps } from "./date"
|
||||||
|
|
||||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||||
const { formatMessage } = useIntl()
|
const intl = useIntl()
|
||||||
const d = useWatch({ name })
|
const d = useWatch({ name })
|
||||||
const { control, setValue } = useFormContext()
|
const { control, setValue } = useFormContext()
|
||||||
const { field } = useController({
|
const { field } = useController({
|
||||||
@@ -26,6 +27,19 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
name,
|
name,
|
||||||
rules: registerOptions,
|
rules: registerOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [dateSegments, setDateSegment] = useState<{
|
||||||
|
year: number | null
|
||||||
|
month: number | null
|
||||||
|
date: number | null
|
||||||
|
daysInMonth: number
|
||||||
|
}>({
|
||||||
|
year: null,
|
||||||
|
month: null,
|
||||||
|
date: null,
|
||||||
|
daysInMonth: 31,
|
||||||
|
})
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
const months = rangeArray(1, 12).map((month) => ({
|
const months = rangeArray(1, 12).map((month) => ({
|
||||||
value: month,
|
value: month,
|
||||||
@@ -41,17 +55,38 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
* must subtract by 1 to get the selected month
|
* must subtract by 1 to get the selected month
|
||||||
*/
|
*/
|
||||||
return (select: Key) => {
|
return (select: Key) => {
|
||||||
if (selector === DateName.month) {
|
const value =
|
||||||
select = Number(select) - 1
|
selector === DateName.month ? Number(select) - 1 : Number(select)
|
||||||
|
const newSegments = { ...dateSegments, [selector]: value }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update daysInMonth when year or month changes
|
||||||
|
* to ensure the user can't select a date that doesn't exist.
|
||||||
|
*/
|
||||||
|
if (selector === DateName.year || selector === DateName.month) {
|
||||||
|
const year = selector === DateName.year ? value : newSegments.year
|
||||||
|
const month = selector === DateName.month ? value : newSegments.month
|
||||||
|
if (year !== null && month !== null) {
|
||||||
|
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const newDate = dt(d).set(selector, Number(select))
|
|
||||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
if (Object.values(newSegments).every((val) => val !== null)) {
|
||||||
|
const newDate = dt()
|
||||||
|
.utc()
|
||||||
|
.set("year", newSegments.year!)
|
||||||
|
.set("month", newSegments.month!)
|
||||||
|
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
|
||||||
|
|
||||||
|
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||||
|
}
|
||||||
|
setDateSegment(newSegments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayLabel = formatMessage({ id: "Day" })
|
const dayLabel = intl.formatMessage({ id: "Day" })
|
||||||
const monthLabel = formatMessage({ id: "Month" })
|
const monthLabel = intl.formatMessage({ id: "Month" })
|
||||||
const yearLabel = formatMessage({ id: "Year" })
|
const yearLabel = intl.formatMessage({ id: "Year" })
|
||||||
|
|
||||||
let dateValue = null
|
let dateValue = null
|
||||||
try {
|
try {
|
||||||
@@ -60,35 +95,30 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
* date, but we can't check isNan since
|
* date, but we can't check isNan since
|
||||||
* we recieve the date as "1999-01-01"
|
* we recieve the date as "1999-01-01"
|
||||||
*/
|
*/
|
||||||
dateValue = parseDate(d)
|
dateValue = dt(d).isValid() ? parseDate(d) : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
aria-label={formatMessage({ id: "Select date of birth" })}
|
aria-label={intl.formatMessage({ id: "Select date of birth" })}
|
||||||
granularity="day"
|
|
||||||
isRequired={!!registerOptions.required}
|
isRequired={!!registerOptions.required}
|
||||||
name={name}
|
name={name}
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
|
data-testid={name}
|
||||||
>
|
>
|
||||||
<Group>
|
<Group>
|
||||||
<DateInput className={styles.container}>
|
<DateInput className={styles.container}>
|
||||||
{(segment) => {
|
{(segment) => {
|
||||||
switch (segment.type) {
|
switch (segment.type) {
|
||||||
case "day":
|
case "day":
|
||||||
let days = []
|
const maxDays = dateSegments.daysInMonth
|
||||||
if (segment.maxValue && segment.minValue) {
|
const days = rangeArray(1, maxDays).map((day) => ({
|
||||||
days = rangeArray(segment.minValue, segment.maxValue).map(
|
value: day,
|
||||||
(day) => ({ value: day, label: `${day}` })
|
label: `${day}`,
|
||||||
)
|
}))
|
||||||
} else {
|
|
||||||
days = Array.from(Array(segment.maxValue).keys()).map(
|
|
||||||
(i) => ({ value: i + 1, label: `${i + 1}` })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.day}>
|
<div className={styles.day}>
|
||||||
<Select
|
<Select
|
||||||
@@ -100,7 +130,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
placeholder="DD"
|
placeholder="DD"
|
||||||
required
|
required
|
||||||
tabIndex={3}
|
tabIndex={3}
|
||||||
value={segment.value}
|
defaultValue={
|
||||||
|
segment.isPlaceholder ? undefined : segment.value
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -116,7 +148,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
placeholder="MM"
|
placeholder="MM"
|
||||||
required
|
required
|
||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
value={segment.value}
|
defaultValue={
|
||||||
|
segment.isPlaceholder ? undefined : segment.value
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -132,7 +166,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
placeholder="YYYY"
|
placeholder="YYYY"
|
||||||
required
|
required
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
value={segment.value}
|
defaultValue={
|
||||||
|
segment.isPlaceholder ? undefined : segment.value
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,33 +1,63 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
import { Text, TextField } from "react-aria-components"
|
import { Text, TextField } from "react-aria-components"
|
||||||
import { Controller, useFormContext } from "react-hook-form"
|
import { Controller, useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { CheckIcon, CloseIcon } from "@/components/Icons"
|
import {
|
||||||
import Error from "@/components/TempDesignSystem/Form/ErrorMessage/Error"
|
CheckIcon,
|
||||||
|
CloseIcon,
|
||||||
|
EyeHideIcon,
|
||||||
|
EyeShowIcon,
|
||||||
|
InfoCircleIcon,
|
||||||
|
} from "@/components/Icons"
|
||||||
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import { passwordValidators } from "@/utils/passwordValidator"
|
||||||
|
|
||||||
import { type IconProps, Key, type NewPasswordProps } from "./newPassword"
|
import Button from "../../Button"
|
||||||
|
import { IconProps, type NewPasswordProps } from "./newPassword"
|
||||||
|
|
||||||
import styles from "./newPassword.module.css"
|
import styles from "./newPassword.module.css"
|
||||||
|
|
||||||
|
import { PasswordValidatorKey } from "@/types/components/form/newPassword"
|
||||||
|
|
||||||
export default function NewPassword({
|
export default function NewPassword({
|
||||||
|
name = "newPassword",
|
||||||
"aria-label": ariaLabel,
|
"aria-label": ariaLabel,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
registerOptions = {},
|
registerOptions = {},
|
||||||
|
label,
|
||||||
}: NewPasswordProps) {
|
}: NewPasswordProps) {
|
||||||
const { control } = useFormContext()
|
const { control } = useFormContext()
|
||||||
const { formatMessage } = useIntl()
|
const intl = useIntl()
|
||||||
|
const [isPasswordVisible, setPasswordVisible] = useState(false)
|
||||||
|
|
||||||
|
function getErrorMessage(key: PasswordValidatorKey) {
|
||||||
|
switch (key) {
|
||||||
|
case "length":
|
||||||
|
return `10 ${intl.formatMessage({ id: "to" })} 40 ${intl.formatMessage({ id: "characters" })}`
|
||||||
|
case "hasUppercase":
|
||||||
|
return `1 ${intl.formatMessage({ id: "uppercase letter" })}`
|
||||||
|
case "hasLowercase":
|
||||||
|
return `1 ${intl.formatMessage({ id: "lowercase letter" })}`
|
||||||
|
case "hasNumber":
|
||||||
|
return `1 ${intl.formatMessage({ id: "number" })}`
|
||||||
|
case "hasSpecialChar":
|
||||||
|
return `1 ${intl.formatMessage({ id: "special character" })}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
control={control}
|
control={control}
|
||||||
name="newPassword"
|
name={name}
|
||||||
rules={registerOptions}
|
rules={registerOptions}
|
||||||
render={({ field, fieldState }) => {
|
render={({ field, fieldState, formState }) => {
|
||||||
const messages = (fieldState.error?.message?.split(",") ?? []) as Key[]
|
const errors = Object.values(formState.errors[name]?.types ?? []).flat()
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
@@ -39,51 +69,47 @@ export default function NewPassword({
|
|||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
validationBehavior="aria"
|
validationBehavior="aria"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
type="password"
|
type={isPasswordVisible ? "text" : "password"}
|
||||||
>
|
>
|
||||||
<AriaInputWithLabel
|
<div className={styles.inputWrapper}>
|
||||||
{...field}
|
<AriaInputWithLabel
|
||||||
aria-labelledby={field.name}
|
{...field}
|
||||||
id={field.name}
|
aria-labelledby={field.name}
|
||||||
label={formatMessage({ id: "New password" })}
|
id={field.name}
|
||||||
placeholder={placeholder}
|
label={intl.formatMessage({ id: "New password" })}
|
||||||
type="password"
|
placeholder={placeholder}
|
||||||
/>
|
type={isPasswordVisible ? "text" : "password"}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className={styles.eyeIcon}
|
||||||
|
type="button"
|
||||||
|
variant="icon"
|
||||||
|
size="small"
|
||||||
|
intent="tertiary"
|
||||||
|
onClick={() => setPasswordVisible(!isPasswordVisible)}
|
||||||
|
>
|
||||||
|
{isPasswordVisible ? <EyeHideIcon /> : <EyeShowIcon />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
<div className={styles.errors}>
|
<div className={styles.errors}>
|
||||||
<Caption asChild color="black">
|
{Object.entries(passwordValidators).map(
|
||||||
<Text className={styles.helpText} slot="description">
|
([key, { message }]) => (
|
||||||
<Icon matcher={Key.CHAR_LENGTH} messages={messages} />
|
<Caption asChild color="black" key={key}>
|
||||||
10 {formatMessage({ id: "to" })} 40{" "}
|
<Text className={styles.helpText} slot="description">
|
||||||
{formatMessage({ id: "characters" })}
|
<Icon errorMessage={message} errors={errors} />
|
||||||
</Text>
|
{getErrorMessage(key as PasswordValidatorKey)}
|
||||||
</Caption>
|
</Text>
|
||||||
<Caption asChild color="black">
|
</Caption>
|
||||||
<Text className={styles.helpText} slot="description">
|
)
|
||||||
<Icon matcher={Key.UPPERCASE} messages={messages} />1{" "}
|
)}
|
||||||
{formatMessage({ id: "uppercase letter" })}
|
|
||||||
</Text>
|
|
||||||
</Caption>
|
|
||||||
<Caption asChild color="black">
|
|
||||||
<Text className={styles.helpText} slot="description">
|
|
||||||
<Icon matcher={Key.NUM} messages={messages} />1{" "}
|
|
||||||
{formatMessage({ id: "number" })}
|
|
||||||
</Text>
|
|
||||||
</Caption>
|
|
||||||
<Caption asChild color="black">
|
|
||||||
<Text className={styles.helpText} slot="description">
|
|
||||||
<Icon matcher={Key.SPECIAL_CHAR} messages={messages} />1{" "}
|
|
||||||
{formatMessage({ id: "special character" })}
|
|
||||||
</Text>
|
|
||||||
</Caption>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!field.value && fieldState.error ? (
|
{!field.value && fieldState.error ? (
|
||||||
<Error>
|
<Caption className={styles.error} fontOnly>
|
||||||
<Text className={styles.helpText} slot="description">
|
<InfoCircleIcon color="red" />
|
||||||
{fieldState.error.message}
|
{fieldState.error.message}
|
||||||
</Text>
|
</Caption>
|
||||||
</Error>
|
|
||||||
) : null}
|
) : null}
|
||||||
</TextField>
|
</TextField>
|
||||||
)
|
)
|
||||||
@@ -92,8 +118,8 @@ export default function NewPassword({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Icon({ matcher, messages }: IconProps) {
|
function Icon({ errorMessage, errors }: IconProps) {
|
||||||
return messages.includes(matcher) ? (
|
return errors.includes(errorMessage) ? (
|
||||||
<CloseIcon color="red" height={20} width={20} />
|
<CloseIcon color="red" height={20} width={20} />
|
||||||
) : (
|
) : (
|
||||||
<CheckIcon color="green" height={20} width={20} />
|
<CheckIcon color="green" height={20} width={20} />
|
||||||
|
|||||||
@@ -1,12 +1,88 @@
|
|||||||
|
.container {
|
||||||
|
align-content: center;
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border-color: var(--Scandic-Beige-40);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
display: grid;
|
||||||
|
height: 60px;
|
||||||
|
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||||
|
transition: border-color 200ms ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container:has(.input:active, .input:focus) {
|
||||||
|
border-color: var(--Scandic-Blue-90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container:has(.input:disabled) {
|
||||||
|
background-color: var(--Main-Grey-10);
|
||||||
|
border: none;
|
||||||
|
color: var(--Main-Grey-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||||
|
border-color: var(--Scandic-Red-60);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--Main-Grey-100);
|
||||||
|
height: 18px;
|
||||||
|
margin: 0;
|
||||||
|
order: 2;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:not(:active, :focus):placeholder-shown {
|
||||||
|
height: 0px;
|
||||||
|
transition: height 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus,
|
||||||
|
.input:focus:placeholder-shown,
|
||||||
|
.input:active,
|
||||||
|
.input:active:placeholder-shown {
|
||||||
|
height: 18px;
|
||||||
|
transition: height 150ms ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled {
|
||||||
|
color: var(--Main-Grey-40);
|
||||||
|
}
|
||||||
|
|
||||||
.helpText {
|
.helpText {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--Scandic-Red-60);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
margin: var(--Spacing-x1) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.errors {
|
.errors {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||||
padding-top: var(--Spacing-x1);
|
padding-top: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eyeIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--Spacing-x2);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import type { RegisterOptions } from "react-hook-form"
|
import type { RegisterOptions } from "react-hook-form"
|
||||||
|
|
||||||
export enum Key {
|
|
||||||
CHAR_LENGTH = "CHAR_LENGTH",
|
|
||||||
NUM = "NUM",
|
|
||||||
SPECIAL_CHAR = "SPECIAL_CHAR",
|
|
||||||
UPPERCASE = "UPPERCASE",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NewPasswordProps
|
export interface NewPasswordProps
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
label?: string
|
label?: string
|
||||||
@@ -14,6 +7,6 @@ export interface NewPasswordProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
matcher: Key
|
errorMessage: string
|
||||||
messages: Key[]
|
errors: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export default function Phone({
|
|||||||
className={styles.select}
|
className={styles.select}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
|
data-testid="country-selector"
|
||||||
>
|
>
|
||||||
<Label required={!!registerOptions.required} size="small">
|
<Label required={!!registerOptions.required} size="small">
|
||||||
{formatMessage({ id: "Country code" })}
|
{formatMessage({ id: "Country code" })}
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ export default function Select({
|
|||||||
disabled={field.disabled}
|
disabled={field.disabled}
|
||||||
items={items}
|
items={items}
|
||||||
label={label}
|
label={label}
|
||||||
|
aria-label={label}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
onSelect={field.onChange}
|
onSelect={field.onChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
|
data-testid={name}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,10 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.baseButtonTextOnFillNormal {
|
||||||
|
color: var(--Base-Button-Text-On-Fill-Normal);
|
||||||
|
}
|
||||||
|
|
||||||
.black {
|
.black {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
@@ -218,3 +222,15 @@
|
|||||||
color: var(--Base-Text-High-contrast);
|
color: var(--Base-Text-High-contrast);
|
||||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.signupVerification {
|
||||||
|
background-color: var(--Base-Button-Primary-Fill-Normal);
|
||||||
|
color: var(--Base-Button-Inverted-Fill-Normal);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const linkVariants = cva(styles.link, {
|
|||||||
true: styles.active,
|
true: styles.active,
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
|
baseButtonTextOnFillNormal: styles.baseButtonTextOnFillNormal,
|
||||||
black: styles.black,
|
black: styles.black,
|
||||||
burgundy: styles.burgundy,
|
burgundy: styles.burgundy,
|
||||||
none: "",
|
none: "",
|
||||||
@@ -35,6 +36,7 @@ export const linkVariants = cva(styles.link, {
|
|||||||
shortcut: styles.shortcut,
|
shortcut: styles.shortcut,
|
||||||
sidebar: styles.sidebar,
|
sidebar: styles.sidebar,
|
||||||
tab: styles.tab,
|
tab: styles.tab,
|
||||||
|
signupVerification: styles.signupVerification,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function Select({
|
|||||||
selectedKey={value as Key}
|
selectedKey={value as Key}
|
||||||
>
|
>
|
||||||
<Body asChild fontOnly>
|
<Body asChild fontOnly>
|
||||||
<Button className={styles.input}>
|
<Button className={styles.input} data-testid={name}>
|
||||||
<div className={styles.inputContentWrapper} tabIndex={tabIndex}>
|
<div className={styles.inputContentWrapper} tabIndex={tabIndex}>
|
||||||
<Label required={required} size="small">
|
<Label required={required} size="small">
|
||||||
{label}
|
{label}
|
||||||
@@ -89,6 +89,7 @@ export default function Select({
|
|||||||
className={styles.listBoxItem}
|
className={styles.listBoxItem}
|
||||||
id={item.value}
|
id={item.value}
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
data-testid={item.label}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</ListBoxItem>
|
</ListBoxItem>
|
||||||
|
|||||||
33
constants/routes/signup.ts
Normal file
33
constants/routes/signup.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { LangRoute } from "@/types/routes"
|
||||||
|
|
||||||
|
export const signup: LangRoute = {
|
||||||
|
en: "/en/scandic-friends/join-scandic-friends",
|
||||||
|
sv: "/sv/scandic-friends/bli-medlem",
|
||||||
|
no: "/no/scandic-friends/registrer-deg-for-scandic-friends",
|
||||||
|
fi: "/fi/scandic-friends/liity-scandic-friends-ohjelmaan",
|
||||||
|
da: "/da/scandic-friends/tilmeld-dig-scandic-friends",
|
||||||
|
de: "/de/scandic-friends/werden-sie-mitglied-von-scandic-friends",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const signupVerify: LangRoute = {
|
||||||
|
en: `${signup.en}/verify`,
|
||||||
|
sv: `${signup.sv}/verifiera`,
|
||||||
|
no: `${signup.no}/bekreft`,
|
||||||
|
fi: `${signup.fi}/vahvista`,
|
||||||
|
da: `${signup.da}/bekraeft`,
|
||||||
|
de: `${signup.de}/verifizieren`,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No work has been scoped out to implement terms of privacy pages for new web yet,
|
||||||
|
* which is why these currently link to routes in current web.
|
||||||
|
* TODO: Align w. stakeholders, make tickets (and reference them here) to update these
|
||||||
|
* links when the time comes.
|
||||||
|
*/
|
||||||
|
export const signupTerms: LangRoute = {
|
||||||
|
en: "https://www.scandichotels.com/customer-service/rates-and-policies/privacy-policy",
|
||||||
|
sv: "https://www.scandichotels.se/kundservice/priser-och-bokningsregler/integritetspolicy",
|
||||||
|
no: "https://www.scandichotels.no/kundeservice/priser-og-bestillingsvilkar/personvernpolicy",
|
||||||
|
fi: "https://www.scandichotels.fi/asiakaspalvelu/hinnat-ja-varausehdot/tietosuojaseloste",
|
||||||
|
da: "https://www.scandichotels.dk/kundeservice/priser-og-bookingvilkar/persondatapolitik",
|
||||||
|
de: "https://www.scandichotels.de/kundenbetreuung/preise-und-richtlinien/datenschutzrichtlinie",
|
||||||
|
}
|
||||||
@@ -17,16 +17,18 @@
|
|||||||
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
|
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
|
||||||
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
|
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
|
||||||
"Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.",
|
"Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.",
|
||||||
|
"Approx.": "Ca.",
|
||||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?",
|
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på, at du vil fjerne kortet, der slutter me {lastFourDigits} fra din medlemsprofil?",
|
||||||
"Arrival date": "Ankomstdato",
|
"Arrival date": "Ankomstdato",
|
||||||
"As our": "Som vores {level}",
|
"As our": "Som vores {level}",
|
||||||
"As our Close Friend": "Som vores nære ven",
|
"As our Close Friend": "Som vores nære ven",
|
||||||
"At latest": "Senest",
|
"At latest": "Senest",
|
||||||
"At the hotel": "På hotellet",
|
"At the hotel": "På hotellet",
|
||||||
"Attraction": "Attraktion",
|
"Attractions": "Attraktioner",
|
||||||
"Back to scandichotels.com": "Tilbage til scandichotels.com",
|
"Back to scandichotels.com": "Tilbage til scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
|
"Birth date": "Fødselsdato",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
"Book reward night": "Book bonusnat",
|
"Book reward night": "Book bonusnat",
|
||||||
"Booking number": "Bookingnummer",
|
"Booking number": "Bookingnummer",
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
"Coming up": "Er lige om hjørnet",
|
"Coming up": "Er lige om hjørnet",
|
||||||
"Compare all levels": "Sammenlign alle niveauer",
|
"Compare all levels": "Sammenlign alle niveauer",
|
||||||
"Complete booking & go to payment": "Udfyld booking & gå til betaling",
|
"Complete booking & go to payment": "Udfyld booking & gå til betaling",
|
||||||
|
"Contact information": "Kontaktoplysninger",
|
||||||
"Contact us": "Kontakt os",
|
"Contact us": "Kontakt os",
|
||||||
"Continue": "Blive ved",
|
"Continue": "Blive ved",
|
||||||
"Copyright all rights reserved": "Scandic AB Alle rettigheder forbeholdes",
|
"Copyright all rights reserved": "Scandic AB Alle rettigheder forbeholdes",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||||
"How it works": "Hvordan det virker",
|
"How it works": "Hvordan det virker",
|
||||||
"Image gallery": "Billedgalleri",
|
"Image gallery": "Billedgalleri",
|
||||||
|
"Included": "Inkluderet",
|
||||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.",
|
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.",
|
||||||
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
|
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
|
||||||
"Join at no cost": "Tilmeld dig uden omkostninger",
|
"Join at no cost": "Tilmeld dig uden omkostninger",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"Log in here": "Log ind her",
|
"Log in here": "Log ind her",
|
||||||
"Log in/Join": "Log på/Tilmeld dig",
|
"Log in/Join": "Log på/Tilmeld dig",
|
||||||
"Log out": "Log ud",
|
"Log out": "Log ud",
|
||||||
|
"lowercase letter": "lille bogstav",
|
||||||
"Main menu": "Hovedmenu",
|
"Main menu": "Hovedmenu",
|
||||||
"Manage preferences": "Administrer præferencer",
|
"Manage preferences": "Administrer præferencer",
|
||||||
"Map": "Kort",
|
"Map": "Kort",
|
||||||
@@ -192,12 +197,14 @@
|
|||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
"Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer",
|
"Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer",
|
||||||
"Points": "Point",
|
"Points": "Point",
|
||||||
|
"points": "Point",
|
||||||
"Points being calculated": "Point udregnes",
|
"Points being calculated": "Point udregnes",
|
||||||
"Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021",
|
"Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021",
|
||||||
"Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.",
|
"Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.",
|
||||||
"Points needed to level up": "Point nødvendige for at stige i niveau",
|
"Points needed to level up": "Point nødvendige for at stige i niveau",
|
||||||
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
|
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
|
||||||
"Previous victories": "Tidligere sejre",
|
"Previous victories": "Tidligere sejre",
|
||||||
|
"Proceed to login": "Fortsæt til login",
|
||||||
"Proceed to payment method": "Fortsæt til betalingsmetode",
|
"Proceed to payment method": "Fortsæt til betalingsmetode",
|
||||||
"Public price from": "Offentlig pris fra",
|
"Public price from": "Offentlig pris fra",
|
||||||
"Public transport": "Offentlig transport",
|
"Public transport": "Offentlig transport",
|
||||||
@@ -221,6 +228,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Søge",
|
"Search": "Søge",
|
||||||
|
"Scandic's Privacy Policy.": "Scandic's integritetspolicy.",
|
||||||
"See all photos": "Se alle billeder",
|
"See all photos": "Se alle billeder",
|
||||||
"See hotel details": "Se hoteloplysninger",
|
"See hotel details": "Se hoteloplysninger",
|
||||||
"See room details": "Se værelsesdetaljer",
|
"See room details": "Se værelsesdetaljer",
|
||||||
@@ -240,6 +248,7 @@
|
|||||||
"Show map": "Vis kort",
|
"Show map": "Vis kort",
|
||||||
"Show more": "Vis mere",
|
"Show more": "Vis mere",
|
||||||
"Sign up bonus": "Velkomstbonus",
|
"Sign up bonus": "Velkomstbonus",
|
||||||
|
"Sign up to Scandic Friends": "Tilmeld dig Scandic Friends",
|
||||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||||
"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 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.",
|
"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.",
|
||||||
@@ -251,11 +260,13 @@
|
|||||||
"Summary": "Opsummering",
|
"Summary": "Opsummering",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.",
|
||||||
|
"Terms and conditions": "Vilkår og betingelser",
|
||||||
"Thank you": "Tak",
|
"Thank you": "Tak",
|
||||||
"Theatre": "Teater",
|
"Theatre": "Teater",
|
||||||
"There are no transactions to display": "Der er ingen transaktioner at vise",
|
"There are no transactions to display": "Der er ingen transaktioner at vise",
|
||||||
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
|
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
|
||||||
"Total Points": "Samlet antal point",
|
"Total Points": "Samlet antal point",
|
||||||
|
"Total incl VAT": "Inkl. moms",
|
||||||
"Tourist": "Turist",
|
"Tourist": "Turist",
|
||||||
"Transaction date": "Overførselsdato",
|
"Transaction date": "Overførselsdato",
|
||||||
"Transactions": "Transaktioner",
|
"Transactions": "Transaktioner",
|
||||||
@@ -285,6 +296,7 @@
|
|||||||
"Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig",
|
"Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig",
|
||||||
"Year": "År",
|
"Year": "År",
|
||||||
"Yes, discard changes": "Ja, kasser ændringer",
|
"Yes, discard changes": "Ja, kasser ændringer",
|
||||||
|
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg accepterer vilkårene for Scandic Friends og forstår, at Scandic vil behandle mine personlige oplysninger i henhold til",
|
||||||
"Yes, remove my card": "Ja, fjern mit kort",
|
"Yes, remove my card": "Ja, fjern mit kort",
|
||||||
"You can always change your mind later and add breakfast at the hotel.": "Du kan altid ombestemme dig senere og tilføje morgenmad på hotellet.",
|
"You can always change your mind later and add breakfast at the hotel.": "Du kan altid ombestemme dig senere og tilføje morgenmad på hotellet.",
|
||||||
"You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.",
|
"You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.",
|
||||||
@@ -317,7 +329,6 @@
|
|||||||
"nights": "nætter",
|
"nights": "nætter",
|
||||||
"number": "nummer",
|
"number": "nummer",
|
||||||
"or": "eller",
|
"or": "eller",
|
||||||
"points": "Point",
|
|
||||||
"special character": "speciel karakter",
|
"special character": "speciel karakter",
|
||||||
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
|
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
|
||||||
"to": "til",
|
"to": "til",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||||
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
|
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
|
||||||
"Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.",
|
"Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.",
|
||||||
|
"Approx.": "Ca.",
|
||||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Möchten Sie die Karte mit der Endung {lastFourDigits} wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
||||||
"Arrival date": "Ankunftsdatum",
|
"Arrival date": "Ankunftsdatum",
|
||||||
"As our": "Als unser {level}",
|
"As our": "Als unser {level}",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"Back to scandichotels.com": "Zurück zu scandichotels.com",
|
"Back to scandichotels.com": "Zurück zu scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
"Bed type": "Bettentyp",
|
"Bed type": "Bettentyp",
|
||||||
|
"Birth date": "Geburtsdatum",
|
||||||
"Book": "Buchen",
|
"Book": "Buchen",
|
||||||
"Book reward night": "Bonusnacht buchen",
|
"Book reward night": "Bonusnacht buchen",
|
||||||
"Booking number": "Buchungsnummer",
|
"Booking number": "Buchungsnummer",
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
"Coming up": "Demnächst",
|
"Coming up": "Demnächst",
|
||||||
"Compare all levels": "Vergleichen Sie alle Levels",
|
"Compare all levels": "Vergleichen Sie alle Levels",
|
||||||
"Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen",
|
"Complete booking & go to payment": "Buchung abschließen & zur Bezahlung gehen",
|
||||||
|
"Contact information": "Kontaktinformationen",
|
||||||
"Contact us": "Kontaktieren Sie uns",
|
"Contact us": "Kontaktieren Sie uns",
|
||||||
"Continue": "Weitermachen",
|
"Continue": "Weitermachen",
|
||||||
"Copyright all rights reserved": "Scandic AB Alle Rechte vorbehalten",
|
"Copyright all rights reserved": "Scandic AB Alle Rechte vorbehalten",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
"How do you want to sleep?": "Wie möchtest du schlafen?",
|
||||||
"How it works": "Wie es funktioniert",
|
"How it works": "Wie es funktioniert",
|
||||||
"Image gallery": "Bildergalerie",
|
"Image gallery": "Bildergalerie",
|
||||||
|
"Included": "Iinklusive",
|
||||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.",
|
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.",
|
||||||
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
|
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
|
||||||
"Join at no cost": "Kostenlos beitreten",
|
"Join at no cost": "Kostenlos beitreten",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"Log in here": "Hier einloggen",
|
"Log in here": "Hier einloggen",
|
||||||
"Log in/Join": "Log in/Anmelden",
|
"Log in/Join": "Log in/Anmelden",
|
||||||
"Log out": "Ausloggen",
|
"Log out": "Ausloggen",
|
||||||
|
"lowercase letter": "Kleinbuchstabe",
|
||||||
"Main menu": "Hauptmenü",
|
"Main menu": "Hauptmenü",
|
||||||
"Manage preferences": "Verwalten von Voreinstellungen",
|
"Manage preferences": "Verwalten von Voreinstellungen",
|
||||||
"Map": "Karte",
|
"Map": "Karte",
|
||||||
@@ -198,6 +203,7 @@
|
|||||||
"Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden",
|
"Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden",
|
||||||
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
|
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
|
||||||
"Previous victories": "Bisherige Siege",
|
"Previous victories": "Bisherige Siege",
|
||||||
|
"Proceed to login": "Weiter zum Login",
|
||||||
"Proceed to payment method": "Weiter zur Zahlungsmethode",
|
"Proceed to payment method": "Weiter zur Zahlungsmethode",
|
||||||
"Public price from": "Öffentlicher Preis ab",
|
"Public price from": "Öffentlicher Preis ab",
|
||||||
"Public transport": "Öffentliche Verkehrsmittel",
|
"Public transport": "Öffentliche Verkehrsmittel",
|
||||||
@@ -221,6 +227,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Suchen",
|
"Search": "Suchen",
|
||||||
|
"Scandic's Privacy Policy.": "Scandics Datenschutzrichtlinie.",
|
||||||
"See all photos": "Alle Fotos ansehen",
|
"See all photos": "Alle Fotos ansehen",
|
||||||
"See hotel details": "Hotelinformationen ansehen",
|
"See hotel details": "Hotelinformationen ansehen",
|
||||||
"See room details": "Zimmerdetails ansehen",
|
"See room details": "Zimmerdetails ansehen",
|
||||||
@@ -240,6 +247,7 @@
|
|||||||
"Show map": "Karte anzeigen",
|
"Show map": "Karte anzeigen",
|
||||||
"Show more": "Mehr anzeigen",
|
"Show more": "Mehr anzeigen",
|
||||||
"Sign up bonus": "Anmelde-Bonus",
|
"Sign up bonus": "Anmelde-Bonus",
|
||||||
|
"Sign up to Scandic Friends": "Treten Sie Scandic Friends bei",
|
||||||
"Skip to main content": "Direkt zum Inhalt",
|
"Skip to main content": "Direkt zum Inhalt",
|
||||||
"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 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.",
|
"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.",
|
||||||
@@ -251,11 +259,13 @@
|
|||||||
"Summary": "Zusammenfassung",
|
"Summary": "Zusammenfassung",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.",
|
||||||
|
"Terms and conditions": "Geschäftsbedingungen",
|
||||||
"Thank you": "Danke",
|
"Thank you": "Danke",
|
||||||
"Theatre": "Theater",
|
"Theatre": "Theater",
|
||||||
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
||||||
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
|
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
|
||||||
"Total Points": "Gesamtpunktzahl",
|
"Total Points": "Gesamtpunktzahl",
|
||||||
|
"Total incl VAT": "Gesamt inkl. MwSt.",
|
||||||
"Tourist": "Tourist",
|
"Tourist": "Tourist",
|
||||||
"Transaction date": "Transaktionsdatum",
|
"Transaction date": "Transaktionsdatum",
|
||||||
"Transactions": "Transaktionen",
|
"Transactions": "Transaktionen",
|
||||||
@@ -285,6 +295,7 @@
|
|||||||
"Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?",
|
"Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?",
|
||||||
"Year": "Jahr",
|
"Year": "Jahr",
|
||||||
"Yes, discard changes": "Ja, Änderungen verwerfen",
|
"Yes, discard changes": "Ja, Änderungen verwerfen",
|
||||||
|
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, ich akzeptiere die Geschäftsbedingungen für Scandic Friends und erkenne an, dass Scandic meine persönlichen Daten in Übereinstimmung mit",
|
||||||
"Yes, remove my card": "Ja, meine Karte entfernen",
|
"Yes, remove my card": "Ja, meine Karte entfernen",
|
||||||
"You can always change your mind later and add breakfast at the hotel.": "Sie können es sich später jederzeit anders überlegen und das Frühstück im Hotel hinzufügen.",
|
"You can always change your mind later and add breakfast at the hotel.": "Sie können es sich später jederzeit anders überlegen und das Frühstück im Hotel hinzufügen.",
|
||||||
"You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.",
|
"You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
|
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
|
||||||
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
|
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
|
||||||
"Any changes you've made will be lost.": "Any changes you've made will be lost.",
|
"Any changes you've made will be lost.": "Any changes you've made will be lost.",
|
||||||
|
"Approx.": "Approx.",
|
||||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?",
|
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?",
|
||||||
"Arrival date": "Arrival date",
|
"Arrival date": "Arrival date",
|
||||||
"As our": "As our {level}",
|
"As our": "As our {level}",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"Back to scandichotels.com": "Back to scandichotels.com",
|
"Back to scandichotels.com": "Back to scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
"Bed type": "Bed type",
|
"Bed type": "Bed type",
|
||||||
|
"Birth date": "Birth date",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
"Book reward night": "Book reward night",
|
"Book reward night": "Book reward night",
|
||||||
"Booking number": "Booking number",
|
"Booking number": "Booking number",
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
"Coming up": "Coming up",
|
"Coming up": "Coming up",
|
||||||
"Compare all levels": "Compare all levels",
|
"Compare all levels": "Compare all levels",
|
||||||
"Complete booking & go to payment": "Complete booking & go to payment",
|
"Complete booking & go to payment": "Complete booking & go to payment",
|
||||||
|
"Contact information": "Contact information",
|
||||||
"Contact us": "Contact us",
|
"Contact us": "Contact us",
|
||||||
"Continue": "Continue",
|
"Continue": "Continue",
|
||||||
"Copyright all rights reserved": "Scandic AB All rights reserved",
|
"Copyright all rights reserved": "Scandic AB All rights reserved",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"How do you want to sleep?": "How do you want to sleep?",
|
"How do you want to sleep?": "How do you want to sleep?",
|
||||||
"How it works": "How it works",
|
"How it works": "How it works",
|
||||||
"Image gallery": "Image gallery",
|
"Image gallery": "Image gallery",
|
||||||
|
"Included": "Included",
|
||||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.",
|
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.",
|
||||||
"Join Scandic Friends": "Join Scandic Friends",
|
"Join Scandic Friends": "Join Scandic Friends",
|
||||||
"Join at no cost": "Join at no cost",
|
"Join at no cost": "Join at no cost",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"Log in here": "Log in here",
|
"Log in here": "Log in here",
|
||||||
"Log in/Join": "Log in/Join",
|
"Log in/Join": "Log in/Join",
|
||||||
"Log out": "Log out",
|
"Log out": "Log out",
|
||||||
|
"lowercase letter": "lowercase letter",
|
||||||
"Main menu": "Main menu",
|
"Main menu": "Main menu",
|
||||||
"Manage preferences": "Manage preferences",
|
"Manage preferences": "Manage preferences",
|
||||||
"Map": "Map",
|
"Map": "Map",
|
||||||
@@ -198,6 +203,7 @@
|
|||||||
"Points needed to level up": "Points needed to level up",
|
"Points needed to level up": "Points needed to level up",
|
||||||
"Points needed to stay on level": "Points needed to stay on level",
|
"Points needed to stay on level": "Points needed to stay on level",
|
||||||
"Previous victories": "Previous victories",
|
"Previous victories": "Previous victories",
|
||||||
|
"Proceed to login": "Proceed to login",
|
||||||
"Proceed to payment method": "Proceed to payment method",
|
"Proceed to payment method": "Proceed to payment method",
|
||||||
"Public price from": "Public price from",
|
"Public price from": "Public price from",
|
||||||
"Public transport": "Public transport",
|
"Public transport": "Public transport",
|
||||||
@@ -220,6 +226,7 @@
|
|||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
|
"Scandic's Privacy Policy.": "Scandic's Privacy Policy.",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"See all photos": "See all photos",
|
"See all photos": "See all photos",
|
||||||
"See hotel details": "See hotel details",
|
"See hotel details": "See hotel details",
|
||||||
@@ -240,6 +247,7 @@
|
|||||||
"Show map": "Show map",
|
"Show map": "Show map",
|
||||||
"Show more": "Show more",
|
"Show more": "Show more",
|
||||||
"Sign up bonus": "Sign up bonus",
|
"Sign up bonus": "Sign up bonus",
|
||||||
|
"Sign up to Scandic Friends": "Sign up to Scandic Friends",
|
||||||
"Skip to main content": "Skip to main content",
|
"Skip to main content": "Skip to main content",
|
||||||
"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 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.",
|
"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.",
|
||||||
@@ -251,11 +259,13 @@
|
|||||||
"Summary": "Summary",
|
"Summary": "Summary",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
|
||||||
|
"Terms and conditions": "Terms and conditions",
|
||||||
"Thank you": "Thank you",
|
"Thank you": "Thank you",
|
||||||
"Theatre": "Theatre",
|
"Theatre": "Theatre",
|
||||||
"There are no transactions to display": "There are no transactions to display",
|
"There are no transactions to display": "There are no transactions to display",
|
||||||
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
|
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
|
||||||
"Total Points": "Total Points",
|
"Total Points": "Total Points",
|
||||||
|
"Total incl VAT": "Total incl VAT",
|
||||||
"Tourist": "Tourist",
|
"Tourist": "Tourist",
|
||||||
"Transaction date": "Transaction date",
|
"Transaction date": "Transaction date",
|
||||||
"Transactions": "Transactions",
|
"Transactions": "Transactions",
|
||||||
@@ -285,6 +295,7 @@
|
|||||||
"Which room class suits you the best?": "Which room class suits you the best?",
|
"Which room class suits you the best?": "Which room class suits you the best?",
|
||||||
"Year": "Year",
|
"Year": "Year",
|
||||||
"Yes, discard changes": "Yes, discard changes",
|
"Yes, discard changes": "Yes, discard changes",
|
||||||
|
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
|
||||||
"Yes, remove my card": "Yes, remove my card",
|
"Yes, remove my card": "Yes, remove my card",
|
||||||
"You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.",
|
"You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.",
|
||||||
"You canceled adding a new credit card.": "You canceled adding a new credit card.",
|
"You canceled adding a new credit card.": "You canceled adding a new credit card.",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
|
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||||
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
|
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
|
||||||
"Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.",
|
"Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.",
|
||||||
|
"Approx.": "N.",
|
||||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?",
|
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Haluatko varmasti poistaa kortin, joka päättyy numeroon {lastFourDigits} jäsenprofiilistasi?",
|
||||||
"Arrival date": "Saapumispäivä",
|
"Arrival date": "Saapumispäivä",
|
||||||
"As our": "{level}-etu",
|
"As our": "{level}-etu",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"Back to scandichotels.com": "Takaisin scandichotels.com",
|
"Back to scandichotels.com": "Takaisin scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
"Bed type": "Vuodetyyppi",
|
"Bed type": "Vuodetyyppi",
|
||||||
|
"Birth date": "Syntymäaika",
|
||||||
"Book": "Varaa",
|
"Book": "Varaa",
|
||||||
"Book reward night": "Kirjapalkinto-ilta",
|
"Book reward night": "Kirjapalkinto-ilta",
|
||||||
"Booking number": "Varausnumero",
|
"Booking number": "Varausnumero",
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
"Coming up": "Tulossa",
|
"Coming up": "Tulossa",
|
||||||
"Compare all levels": "Vertaa kaikkia tasoja",
|
"Compare all levels": "Vertaa kaikkia tasoja",
|
||||||
"Complete booking & go to payment": "Täydennä varaus & siirry maksamaan",
|
"Complete booking & go to payment": "Täydennä varaus & siirry maksamaan",
|
||||||
|
"Contact information": "Yhteystiedot",
|
||||||
"Contact us": "Ota meihin yhteyttä",
|
"Contact us": "Ota meihin yhteyttä",
|
||||||
"Continue": "Jatkaa",
|
"Continue": "Jatkaa",
|
||||||
"Copyright all rights reserved": "Scandic AB Kaikki oikeudet pidätetään",
|
"Copyright all rights reserved": "Scandic AB Kaikki oikeudet pidätetään",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
"How do you want to sleep?": "Kuinka haluat nukkua?",
|
||||||
"How it works": "Kuinka se toimii",
|
"How it works": "Kuinka se toimii",
|
||||||
"Image gallery": "Kuvagalleria",
|
"Image gallery": "Kuvagalleria",
|
||||||
|
"Included": "Sisälly hintaan",
|
||||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
|
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
|
||||||
"Join Scandic Friends": "Liity jäseneksi",
|
"Join Scandic Friends": "Liity jäseneksi",
|
||||||
"Join at no cost": "Liity maksutta",
|
"Join at no cost": "Liity maksutta",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"Log in here": "Kirjaudu sisään",
|
"Log in here": "Kirjaudu sisään",
|
||||||
"Log in/Join": "Kirjaudu sisään/Liittyä",
|
"Log in/Join": "Kirjaudu sisään/Liittyä",
|
||||||
"Log out": "Kirjaudu ulos",
|
"Log out": "Kirjaudu ulos",
|
||||||
|
"lowercase letter": "pien kirjain",
|
||||||
"Main menu": "Päävalikko",
|
"Main menu": "Päävalikko",
|
||||||
"Manage preferences": "Asetusten hallinta",
|
"Manage preferences": "Asetusten hallinta",
|
||||||
"Map": "Kartta",
|
"Map": "Kartta",
|
||||||
@@ -192,12 +197,14 @@
|
|||||||
"Phone number": "Puhelinnumero",
|
"Phone number": "Puhelinnumero",
|
||||||
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
|
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
|
||||||
"Points": "Pisteet",
|
"Points": "Pisteet",
|
||||||
|
"points": "pistettä",
|
||||||
"Points being calculated": "Pisteitä lasketaan",
|
"Points being calculated": "Pisteitä lasketaan",
|
||||||
"Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021",
|
"Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021",
|
||||||
"Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.",
|
"Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.",
|
||||||
"Points needed to level up": "Tarvitset vielä",
|
"Points needed to level up": "Tarvitset vielä",
|
||||||
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
|
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
|
||||||
"Previous victories": "Edelliset voitot",
|
"Previous victories": "Edelliset voitot",
|
||||||
|
"Proceed to login": "Jatka kirjautumiseen",
|
||||||
"Proceed to payment method": "Siirry maksutavalle",
|
"Proceed to payment method": "Siirry maksutavalle",
|
||||||
"Public price from": "Julkinen hinta alkaen",
|
"Public price from": "Julkinen hinta alkaen",
|
||||||
"Public transport": "Julkinen liikenne",
|
"Public transport": "Julkinen liikenne",
|
||||||
@@ -222,6 +229,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Haku",
|
"Search": "Haku",
|
||||||
|
"Scandic's Privacy Policy.": "Scandicin tietosuojavalmioksi.",
|
||||||
"See all photos": "Katso kaikki kuvat",
|
"See all photos": "Katso kaikki kuvat",
|
||||||
"See hotel details": "Katso hotellin tiedot",
|
"See hotel details": "Katso hotellin tiedot",
|
||||||
"See room details": "Katso huoneen tiedot",
|
"See room details": "Katso huoneen tiedot",
|
||||||
@@ -241,6 +249,7 @@
|
|||||||
"Show map": "Näytä kartta",
|
"Show map": "Näytä kartta",
|
||||||
"Show more": "Näytä lisää",
|
"Show more": "Näytä lisää",
|
||||||
"Sign up bonus": "Liittymisbonus",
|
"Sign up bonus": "Liittymisbonus",
|
||||||
|
"Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi",
|
||||||
"Skip to main content": "Siirry pääsisältöön",
|
"Skip to main content": "Siirry pääsisältöön",
|
||||||
"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 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.",
|
"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.",
|
||||||
@@ -252,11 +261,13 @@
|
|||||||
"Summary": "Yhteenveto",
|
"Summary": "Yhteenveto",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
|
||||||
|
"Terms and conditions": "Käyttöehdot",
|
||||||
"Thank you": "Kiitos",
|
"Thank you": "Kiitos",
|
||||||
"Theatre": "Teatteri",
|
"Theatre": "Teatteri",
|
||||||
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
||||||
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
|
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
|
||||||
"Total Points": "Kokonaispisteet",
|
"Total Points": "Kokonaispisteet",
|
||||||
|
"Total incl VAT": "Yhteensä sis. alv",
|
||||||
"Tourist": "Turisti",
|
"Tourist": "Turisti",
|
||||||
"Transaction date": "Tapahtuman päivämäärä",
|
"Transaction date": "Tapahtuman päivämäärä",
|
||||||
"Transactions": "Tapahtumat",
|
"Transactions": "Tapahtumat",
|
||||||
@@ -286,6 +297,7 @@
|
|||||||
"Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?",
|
"Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?",
|
||||||
"Year": "Vuosi",
|
"Year": "Vuosi",
|
||||||
"Yes, discard changes": "Kyllä, hylkää muutokset",
|
"Yes, discard changes": "Kyllä, hylkää muutokset",
|
||||||
|
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Kyllä, hyväksyn Scandic Friends -käyttöehdot ja ymmärrän, että Scandic käsittelee minun henkilötietoni asianmukaisesti",
|
||||||
"Yes, remove my card": "Kyllä, poista korttini",
|
"Yes, remove my card": "Kyllä, poista korttini",
|
||||||
"You can always change your mind later and add breakfast at the hotel.": "Voit aina muuttaa mieltäsi myöhemmin ja lisätä aamiaisen hotelliin.",
|
"You can always change your mind later and add breakfast at the hotel.": "Voit aina muuttaa mieltäsi myöhemmin ja lisätä aamiaisen hotelliin.",
|
||||||
"You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.",
|
"You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.",
|
||||||
@@ -318,7 +330,6 @@
|
|||||||
"nights": "yötä",
|
"nights": "yötä",
|
||||||
"number": "määrä",
|
"number": "määrä",
|
||||||
"or": "tai",
|
"or": "tai",
|
||||||
"points": "pistettä",
|
|
||||||
"special character": "erikoishahmo",
|
"special character": "erikoishahmo",
|
||||||
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
|
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
|
||||||
"to": "to",
|
"to": "to",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
|
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
|
||||||
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
|
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
|
||||||
"Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.",
|
"Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.",
|
||||||
|
"Approx.": "Ca.",
|
||||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?",
|
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Er du sikker på at du vil fjerne kortet som slutter på {lastFourDigits} fra medlemsprofilen din?",
|
||||||
"Arrival date": "Ankomstdato",
|
"Arrival date": "Ankomstdato",
|
||||||
"As our": "Som vår {level}",
|
"As our": "Som vår {level}",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"Back to scandichotels.com": "Tilbake til scandichotels.com",
|
"Back to scandichotels.com": "Tilbake til scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
|
"Birth date": "Fødselsdato",
|
||||||
"Book": "Bestill",
|
"Book": "Bestill",
|
||||||
"Book reward night": "Bestill belønningskveld",
|
"Book reward night": "Bestill belønningskveld",
|
||||||
"Booking number": "Bestillingsnummer",
|
"Booking number": "Bestillingsnummer",
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
"Coming up": "Kommer opp",
|
"Coming up": "Kommer opp",
|
||||||
"Compare all levels": "Sammenlign alle nivåer",
|
"Compare all levels": "Sammenlign alle nivåer",
|
||||||
"Complete booking & go to payment": "Fullfør bestilling & gå til betaling",
|
"Complete booking & go to payment": "Fullfør bestilling & gå til betaling",
|
||||||
|
"Contact information": "Kontaktinformasjon",
|
||||||
"Contact us": "Kontakt oss",
|
"Contact us": "Kontakt oss",
|
||||||
"Continue": "Fortsette",
|
"Continue": "Fortsette",
|
||||||
"Copyright all rights reserved": "Scandic AB Alle rettigheter forbeholdt",
|
"Copyright all rights reserved": "Scandic AB Alle rettigheter forbeholdt",
|
||||||
@@ -115,6 +118,7 @@
|
|||||||
"How do you want to sleep?": "Hvordan vil du sove?",
|
"How do you want to sleep?": "Hvordan vil du sove?",
|
||||||
"How it works": "Hvordan det fungerer",
|
"How it works": "Hvordan det fungerer",
|
||||||
"Image gallery": "Bildegalleri",
|
"Image gallery": "Bildegalleri",
|
||||||
|
"Included": "Inkludert",
|
||||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.",
|
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.",
|
||||||
"Join Scandic Friends": "Bli med i Scandic Friends",
|
"Join Scandic Friends": "Bli med i Scandic Friends",
|
||||||
"Join at no cost": "Bli med uten kostnad",
|
"Join at no cost": "Bli med uten kostnad",
|
||||||
@@ -137,6 +141,7 @@
|
|||||||
"Log in here": "Logg inn her",
|
"Log in here": "Logg inn her",
|
||||||
"Log in/Join": "Logg på/Bli med",
|
"Log in/Join": "Logg på/Bli med",
|
||||||
"Log out": "Logg ut",
|
"Log out": "Logg ut",
|
||||||
|
"lowercase letter": "liten bokstav",
|
||||||
"Main menu": "Hovedmeny",
|
"Main menu": "Hovedmeny",
|
||||||
"Manage preferences": "Administrer preferanser",
|
"Manage preferences": "Administrer preferanser",
|
||||||
"Map": "Kart",
|
"Map": "Kart",
|
||||||
@@ -197,6 +202,7 @@
|
|||||||
"Points needed to level up": "Poeng som trengs for å komme opp i nivå",
|
"Points needed to level up": "Poeng som trengs for å komme opp i nivå",
|
||||||
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
|
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
|
||||||
"Previous victories": "Tidligere seire",
|
"Previous victories": "Tidligere seire",
|
||||||
|
"Proceed to login": "Fortsett til innlogging",
|
||||||
"Proceed to payment method": "Fortsett til betalingsmetode",
|
"Proceed to payment method": "Fortsett til betalingsmetode",
|
||||||
"Public price from": "Offentlig pris fra",
|
"Public price from": "Offentlig pris fra",
|
||||||
"Public transport": "Offentlig transport",
|
"Public transport": "Offentlig transport",
|
||||||
@@ -220,6 +226,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Søk",
|
"Search": "Søk",
|
||||||
|
"Scandic's Privacy Policy.": "Scandics integritetspolicy.",
|
||||||
"See all photos": "Se alle bilder",
|
"See all photos": "Se alle bilder",
|
||||||
"See hotel details": "Se hotellinformasjon",
|
"See hotel details": "Se hotellinformasjon",
|
||||||
"See room details": "Se detaljer om rommet",
|
"See room details": "Se detaljer om rommet",
|
||||||
@@ -239,6 +246,7 @@
|
|||||||
"Show map": "Vis kart",
|
"Show map": "Vis kart",
|
||||||
"Show more": "Vis mer",
|
"Show more": "Vis mer",
|
||||||
"Sign up bonus": "Velkomstbonus",
|
"Sign up bonus": "Velkomstbonus",
|
||||||
|
"Sign up to Scandic Friends": "Bli med i Scandic Friends",
|
||||||
"Skip to main content": "Gå videre til hovedsiden",
|
"Skip to main content": "Gå videre til hovedsiden",
|
||||||
"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 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.",
|
"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.",
|
||||||
@@ -250,11 +258,13 @@
|
|||||||
"Summary": "Sammendrag",
|
"Summary": "Sammendrag",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.",
|
||||||
|
"Terms and conditions": "Vilkår og betingelser",
|
||||||
"Thank you": "Takk",
|
"Thank you": "Takk",
|
||||||
"Theatre": "Teater",
|
"Theatre": "Teater",
|
||||||
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
||||||
"Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}",
|
"Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}",
|
||||||
"Total Points": "Totale poeng",
|
"Total Points": "Totale poeng",
|
||||||
|
"Total incl VAT": "Sum inkl mva",
|
||||||
"Tourist": "Turist",
|
"Tourist": "Turist",
|
||||||
"Transaction date": "Transaksjonsdato",
|
"Transaction date": "Transaksjonsdato",
|
||||||
"Transactions": "Transaksjoner",
|
"Transactions": "Transaksjoner",
|
||||||
@@ -284,6 +294,7 @@
|
|||||||
"Which room class suits you the best?": "Hvilken romklasse passer deg best?",
|
"Which room class suits you the best?": "Hvilken romklasse passer deg best?",
|
||||||
"Year": "År",
|
"Year": "År",
|
||||||
"Yes, discard changes": "Ja, forkast endringer",
|
"Yes, discard changes": "Ja, forkast endringer",
|
||||||
|
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg aksepterer vilkårene for Scandic Friends og forstår at Scandic vil behandle mine personlige opplysninger i henhold til",
|
||||||
"Yes, remove my card": "Ja, fjern kortet mitt",
|
"Yes, remove my card": "Ja, fjern kortet mitt",
|
||||||
"You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ombestemme deg senere og legge til frokost på hotellet.",
|
"You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ombestemme deg senere og legge til frokost på hotellet.",
|
||||||
"You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.",
|
"You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
|
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
|
||||||
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
|
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
|
||||||
"Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.",
|
"Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.",
|
||||||
|
"Approx.": "Ca.",
|
||||||
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?",
|
"Are you sure you want to remove the card ending with {lastFourDigits} from your member profile?": "Är du säker på att du vill ta bort kortet som slutar med {lastFourDigits} från din medlemsprofil?",
|
||||||
"Arrival date": "Ankomstdatum",
|
"Arrival date": "Ankomstdatum",
|
||||||
"As our": "Som vår {level}",
|
"As our": "Som vår {level}",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"Back to scandichotels.com": "Tillbaka till scandichotels.com",
|
"Back to scandichotels.com": "Tillbaka till scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
"Bed type": "Sängtyp",
|
"Bed type": "Sängtyp",
|
||||||
|
"Birth date": "Födelsedatum",
|
||||||
"Book": "Boka",
|
"Book": "Boka",
|
||||||
"Book reward night": "Boka frinatt",
|
"Book reward night": "Boka frinatt",
|
||||||
"Booking number": "Bokningsnummer",
|
"Booking number": "Bokningsnummer",
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
"Coming up": "Kommer härnäst",
|
"Coming up": "Kommer härnäst",
|
||||||
"Compare all levels": "Jämför alla nivåer",
|
"Compare all levels": "Jämför alla nivåer",
|
||||||
"Complete booking & go to payment": "Fullför bokning & gå till betalning",
|
"Complete booking & go to payment": "Fullför bokning & gå till betalning",
|
||||||
|
"Contact information": "Kontaktinformation",
|
||||||
"Contact us": "Kontakta oss",
|
"Contact us": "Kontakta oss",
|
||||||
"Continue": "Fortsätt",
|
"Continue": "Fortsätt",
|
||||||
"Copyright all rights reserved": "Scandic AB Alla rättigheter förbehålls",
|
"Copyright all rights reserved": "Scandic AB Alla rättigheter förbehålls",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"How do you want to sleep?": "Hur vill du sova?",
|
"How do you want to sleep?": "Hur vill du sova?",
|
||||||
"How it works": "Hur det fungerar",
|
"How it works": "Hur det fungerar",
|
||||||
"Image gallery": "Bildgalleri",
|
"Image gallery": "Bildgalleri",
|
||||||
|
"Included": "Inkluderad",
|
||||||
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.",
|
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.",
|
||||||
"Join Scandic Friends": "Gå med i Scandic Friends",
|
"Join Scandic Friends": "Gå med i Scandic Friends",
|
||||||
"Join at no cost": "Gå med utan kostnad",
|
"Join at no cost": "Gå med utan kostnad",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"Log in here": "Logga in här",
|
"Log in here": "Logga in här",
|
||||||
"Log in/Join": "Logga in/Gå med",
|
"Log in/Join": "Logga in/Gå med",
|
||||||
"Log out": "Logga ut",
|
"Log out": "Logga ut",
|
||||||
|
"lowercase letter": "liten bokstav",
|
||||||
"Main menu": "Huvudmeny",
|
"Main menu": "Huvudmeny",
|
||||||
"Manage preferences": "Hantera inställningar",
|
"Manage preferences": "Hantera inställningar",
|
||||||
"Map": "Karta",
|
"Map": "Karta",
|
||||||
@@ -198,6 +203,7 @@
|
|||||||
"Points needed to level up": "Poäng som behövs för att gå upp i nivå",
|
"Points needed to level up": "Poäng som behövs för att gå upp i nivå",
|
||||||
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
|
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
|
||||||
"Previous victories": "Tidigare segrar",
|
"Previous victories": "Tidigare segrar",
|
||||||
|
"Proceed to login": "Fortsätt till inloggning",
|
||||||
"Proceed to payment method": "Gå vidare till betalningsmetod",
|
"Proceed to payment method": "Gå vidare till betalningsmetod",
|
||||||
"Public price from": "Offentligt pris från",
|
"Public price from": "Offentligt pris från",
|
||||||
"Public transport": "Kollektivtrafik",
|
"Public transport": "Kollektivtrafik",
|
||||||
@@ -221,6 +227,7 @@
|
|||||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||||
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
"Scandic Friends Point Shop": "Scandic Friends Point Shop",
|
||||||
"Search": "Sök",
|
"Search": "Sök",
|
||||||
|
"Scandic's Privacy Policy.": "Scandics integritetspolicy.",
|
||||||
"See all photos": "Se alla foton",
|
"See all photos": "Se alla foton",
|
||||||
"See hotel details": "Se hotellinformation",
|
"See hotel details": "Se hotellinformation",
|
||||||
"See room details": "Se rumsdetaljer",
|
"See room details": "Se rumsdetaljer",
|
||||||
@@ -240,6 +247,7 @@
|
|||||||
"Show map": "Visa karta",
|
"Show map": "Visa karta",
|
||||||
"Show more": "Visa mer",
|
"Show more": "Visa mer",
|
||||||
"Sign up bonus": "Välkomstbonus",
|
"Sign up bonus": "Välkomstbonus",
|
||||||
|
"Sign up to Scandic Friends": "Bli medlem i Scandic Friends",
|
||||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||||
"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 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.",
|
"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.",
|
||||||
@@ -251,11 +259,13 @@
|
|||||||
"Summary": "Sammanfattning",
|
"Summary": "Sammanfattning",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.",
|
||||||
|
"Terms and conditions": "Allmänna villkor",
|
||||||
"Thank you": "Tack",
|
"Thank you": "Tack",
|
||||||
"Theatre": "Teater",
|
"Theatre": "Teater",
|
||||||
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
||||||
"Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}",
|
"Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}",
|
||||||
"Total Points": "Poäng totalt",
|
"Total Points": "Poäng totalt",
|
||||||
|
"Total incl VAT": "Totalt inkl moms",
|
||||||
"Tourist": "Turist",
|
"Tourist": "Turist",
|
||||||
"Transaction date": "Transaktionsdatum",
|
"Transaction date": "Transaktionsdatum",
|
||||||
"Transactions": "Transaktioner",
|
"Transactions": "Transaktioner",
|
||||||
@@ -285,6 +295,7 @@
|
|||||||
"Which room class suits you the best?": "Vilken rumsklass passar dig bäst?",
|
"Which room class suits you the best?": "Vilken rumsklass passar dig bäst?",
|
||||||
"Year": "År",
|
"Year": "År",
|
||||||
"Yes, discard changes": "Ja, ignorera ändringar",
|
"Yes, discard changes": "Ja, ignorera ändringar",
|
||||||
|
"Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att bearbeta mina personliga uppgifter i enlighet med",
|
||||||
"Yes, remove my card": "Ja, ta bort mitt kort",
|
"Yes, remove my card": "Ja, ta bort mitt kort",
|
||||||
"You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ändra dig senare och lägga till frukost på hotellet.",
|
"You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ändra dig senare och lägga till frukost på hotellet.",
|
||||||
"You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.",
|
"You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.",
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -60,6 +60,7 @@
|
|||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@testing-library/jest-dom": "^6.4.6",
|
"@testing-library/jest-dom": "^6.4.6",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
@@ -6167,6 +6168,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/user-event": {
|
||||||
|
"version": "14.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz",
|
||||||
|
"integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": ">=7.21.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@testing-library/jest-dom": "^6.4.6",
|
"@testing-library/jest-dom": "^6.4.6",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
|
|||||||
8
public/_static/img/icons/eye-show.svg
Normal file
8
public/_static/img/icons/eye-show.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_348_1004" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||||
|
<rect width="24" height="24" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_348_1004)">
|
||||||
|
<path d="M12.0001 15.85C13.2084 15.85 14.2355 15.4271 15.0813 14.5813C15.9272 13.7354 16.3501 12.7083 16.3501 11.5C16.3501 10.2917 15.9272 9.2646 15.0813 8.41876C14.2355 7.57293 13.2084 7.15001 12.0001 7.15001C10.7918 7.15001 9.76468 7.57293 8.91885 8.41876C8.07302 9.2646 7.6501 10.2917 7.6501 11.5C7.6501 12.7083 8.07302 13.7354 8.91885 14.5813C9.76468 15.4271 10.7918 15.85 12.0001 15.85ZM12.003 14.15C11.2677 14.15 10.6418 13.8927 10.1251 13.378C9.60843 12.8632 9.3501 12.2382 9.3501 11.503C9.3501 10.7677 9.60745 10.1417 10.1221 9.62501C10.6369 9.10835 11.2619 8.85001 11.9971 8.85001C12.7324 8.85001 13.3584 9.10736 13.8751 9.62206C14.3918 10.1368 14.6501 10.7618 14.6501 11.4971C14.6501 12.2324 14.3927 12.8583 13.878 13.375C13.3633 13.8917 12.7383 14.15 12.003 14.15ZM12.002 18.8C9.82572 18.8 7.83968 18.2125 6.04385 17.0375C4.24801 15.8625 2.8251 14.3167 1.7751 12.4C1.69176 12.2583 1.63135 12.1124 1.59385 11.9622C1.55635 11.812 1.5376 11.6578 1.5376 11.4997C1.5376 11.3416 1.55635 11.1875 1.59385 11.0375C1.63135 10.8875 1.69176 10.7417 1.7751 10.6C2.8251 8.68335 4.24739 7.13751 6.04197 5.96251C7.83657 4.78751 9.82199 4.20001 11.9982 4.20001C14.1745 4.20001 16.1605 4.78751 17.9563 5.96251C19.7522 7.13751 21.1751 8.68335 22.2251 10.6C22.3084 10.7417 22.3688 10.8876 22.4063 11.0378C22.4438 11.1881 22.4626 11.3422 22.4626 11.5003C22.4626 11.6585 22.4438 11.8125 22.4063 11.9625C22.3688 12.1125 22.3084 12.2583 22.2251 12.4C21.1751 14.3167 19.7528 15.8625 17.9582 17.0375C16.1636 18.2125 14.1782 18.8 12.002 18.8ZM12 16.925C13.8584 16.925 15.5647 16.4375 17.1188 15.4625C18.673 14.4875 19.8584 13.1667 20.6751 11.5C19.8584 9.83335 18.673 8.51251 17.1189 7.53751C15.5648 6.56251 13.8585 6.07501 12.0001 6.07501C10.1418 6.07501 8.43551 6.56251 6.88135 7.53751C5.32718 8.51251 4.14176 9.83335 3.3251 11.5C4.14176 13.1667 5.32716 14.4875 6.8813 15.4625C8.43541 16.4375 10.1417 16.925 12 16.925Z" fill="#A8A4A2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -140,6 +140,20 @@ export const serverActionProcedure = t.procedure.experimental_caller(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const serviceServerActionProcedure = serverActionProcedure.use(
|
||||||
|
async (opts) => {
|
||||||
|
const { access_token } = await getServiceToken()
|
||||||
|
if (!access_token) {
|
||||||
|
throw internalServerError("Failed to obtain service token")
|
||||||
|
}
|
||||||
|
return opts.next({
|
||||||
|
ctx: {
|
||||||
|
serviceToken: access_token,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const protectedServerActionProcedure = serverActionProcedure.use(
|
export const protectedServerActionProcedure = serverActionProcedure.use(
|
||||||
async (opts) => {
|
async (opts) => {
|
||||||
const session = await opts.ctx.auth()
|
const session = await opts.ctx.auth()
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ export interface LoyaltyLevelsProps extends PartialDynamicContent {
|
|||||||
export interface OverviewTableProps extends PartialDynamicContent {
|
export interface OverviewTableProps extends PartialDynamicContent {
|
||||||
firstItem: boolean
|
firstItem: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SignupFormWrapperProps extends PartialDynamicContent {}
|
||||||
|
|
||||||
|
export interface SignUpVerificationProps extends PartialDynamicContent {}
|
||||||
|
|||||||
3
types/components/form/newPassword.ts
Normal file
3
types/components/form/newPassword.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { passwordValidators } from "@/utils/passwordValidator"
|
||||||
|
|
||||||
|
export type PasswordValidatorKey = keyof typeof passwordValidators
|
||||||
5
types/components/form/registerForm.ts
Normal file
5
types/components/form/registerForm.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type RegisterFormProps = {
|
||||||
|
link?: { href: string; text: string }
|
||||||
|
subtitle?: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
@@ -32,6 +32,8 @@ export enum IconName {
|
|||||||
DoorOpen = "DoorOpen",
|
DoorOpen = "DoorOpen",
|
||||||
ElectricBike = "ElectricBike",
|
ElectricBike = "ElectricBike",
|
||||||
Email = "Email",
|
Email = "Email",
|
||||||
|
EyeHide = "EyeHide",
|
||||||
|
EyeShow = "EyeShow",
|
||||||
Facebook = "Facebook",
|
Facebook = "Facebook",
|
||||||
Fitness = "Fitness",
|
Fitness = "Fitness",
|
||||||
Gift = "Gift",
|
Gift = "Gift",
|
||||||
|
|||||||
@@ -78,3 +78,4 @@ export type TrackingPosition =
|
|||||||
| "top menu"
|
| "top menu"
|
||||||
| "hamburger menu"
|
| "hamburger menu"
|
||||||
| "join scandic friends sidebar"
|
| "join scandic friends sidebar"
|
||||||
|
| "sign up verification"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export namespace DynamicContentEnum {
|
|||||||
overview_table = "overview_table",
|
overview_table = "overview_table",
|
||||||
points_overview = "points_overview",
|
points_overview = "points_overview",
|
||||||
previous_stays = "previous_stays",
|
previous_stays = "previous_stays",
|
||||||
|
sign_up_form = "sign_up_form",
|
||||||
|
sign_up_verification = "sign_up_verification",
|
||||||
soonest_stays = "soonest_stays",
|
soonest_stays = "soonest_stays",
|
||||||
upcoming_stays = "upcoming_stays",
|
upcoming_stays = "upcoming_stays",
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,8 @@ export namespace DynamicContentEnum {
|
|||||||
components.overview_table,
|
components.overview_table,
|
||||||
components.points_overview,
|
components.points_overview,
|
||||||
components.previous_stays,
|
components.previous_stays,
|
||||||
|
components.sign_up_form,
|
||||||
|
components.sign_up_verification,
|
||||||
components.soonest_stays,
|
components.soonest_stays,
|
||||||
components.upcoming_stays,
|
components.upcoming_stays,
|
||||||
]
|
]
|
||||||
|
|||||||
46
utils/passwordValidator.ts
Normal file
46
utils/passwordValidator.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const passwordValidators = {
|
||||||
|
length: {
|
||||||
|
matcher: (password: string) =>
|
||||||
|
password.length >= 10 && password.length <= 40,
|
||||||
|
message: "10 to 40 characters",
|
||||||
|
},
|
||||||
|
hasUppercase: {
|
||||||
|
matcher: (password: string) => /[A-Z]/.test(password),
|
||||||
|
message: "1 uppercase letter",
|
||||||
|
},
|
||||||
|
hasLowercase: {
|
||||||
|
matcher: (password: string) => /[a-z]/.test(password),
|
||||||
|
message: "1 lowercase letter",
|
||||||
|
},
|
||||||
|
hasNumber: {
|
||||||
|
matcher: (password: string) => /[0-9]/.test(password),
|
||||||
|
message: "1 number",
|
||||||
|
},
|
||||||
|
hasSpecialChar: {
|
||||||
|
matcher: (password: string) =>
|
||||||
|
/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(password),
|
||||||
|
message: "1 special character",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const passwordValidator = (msg = "Required field") =>
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.min(1, msg)
|
||||||
|
.refine(passwordValidators.length.matcher, {
|
||||||
|
message: passwordValidators.length.message,
|
||||||
|
})
|
||||||
|
.refine(passwordValidators.hasUppercase.matcher, {
|
||||||
|
message: passwordValidators.hasUppercase.message,
|
||||||
|
})
|
||||||
|
.refine(passwordValidators.hasLowercase.matcher, {
|
||||||
|
message: passwordValidators.hasLowercase.message,
|
||||||
|
})
|
||||||
|
.refine(passwordValidators.hasNumber.matcher, {
|
||||||
|
message: passwordValidators.hasNumber.message,
|
||||||
|
})
|
||||||
|
.refine(passwordValidators.hasSpecialChar.matcher, {
|
||||||
|
message: passwordValidators.hasSpecialChar.message,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user