Merged develop into feat/setup-hotel-api-call
This commit is contained in:
@@ -1,25 +1,41 @@
|
||||
"use server"
|
||||
|
||||
// import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
|
||||
|
||||
import { type State, Status } from "@/types/components/myPages/myProfile/edit"
|
||||
|
||||
export async function editProfile(_prevState: State, values: FormData) {
|
||||
try {
|
||||
const data = Object.fromEntries(values.entries())
|
||||
const data: Record<string, any> = Object.fromEntries(values.entries())
|
||||
|
||||
/**
|
||||
* TODO: Update profile data when endpoint from
|
||||
* API team is ready
|
||||
*/
|
||||
|
||||
console.info(`EditProfileSchema.Parse Data`)
|
||||
console.info(`Raw Data`)
|
||||
console.log(data)
|
||||
|
||||
return {
|
||||
message: "All good!",
|
||||
status: Status.success,
|
||||
data.address = {
|
||||
city: data["address.city"],
|
||||
countryCode: data["address.countryCode"],
|
||||
streetAddress: data["address.streetAddress"],
|
||||
zipCode: data["address.zipCode"],
|
||||
}
|
||||
const parsedData = editProfileSchema.safeParse(data)
|
||||
if (parsedData.success) {
|
||||
console.info(`Success`)
|
||||
console.log(parsedData.data)
|
||||
return {
|
||||
message: "All good!",
|
||||
status: Status.success,
|
||||
}
|
||||
} else {
|
||||
console.error(parsedData.error)
|
||||
return {
|
||||
message: "Invalid data, parse failed!",
|
||||
status: Status.error,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { languageSelect } from "@/constants/languages"
|
||||
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 Select from "@/components/TempDesignSystem/Form/Select"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
@@ -21,8 +22,8 @@ export default function FormContent() {
|
||||
const country = formatMessage({ id: "Country" })
|
||||
const email = `${formatMessage({ id: "Email" })} ${formatMessage({ id: "Address" }).toLowerCase()}`
|
||||
const street = formatMessage({ id: "Address" })
|
||||
const phoneNumber = formatMessage({ id: "Phone number" })
|
||||
const password = formatMessage({ id: "Current password" })
|
||||
const newPassword = formatMessage({ id: "New password" })
|
||||
const retypeNewPassword = formatMessage({ id: "Retype new password" })
|
||||
const zipCode = formatMessage({ id: "Zip code" })
|
||||
|
||||
@@ -46,7 +47,7 @@ export default function FormContent() {
|
||||
label={zipCode}
|
||||
name="address.zipCode"
|
||||
placeholder={zipCode}
|
||||
required
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<CountrySelect
|
||||
label={country}
|
||||
@@ -55,12 +56,17 @@ export default function FormContent() {
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
</div>
|
||||
<Input label={email} name="email" placeholder={email} required />
|
||||
<Phone
|
||||
label={formatMessage({ id: "Phone number" })}
|
||||
name="phoneNumber"
|
||||
placeholder={formatMessage({ id: "Phone number" })}
|
||||
<Input
|
||||
label={email}
|
||||
name="email"
|
||||
placeholder={email}
|
||||
registerOptions={{ required: true }}
|
||||
type="email"
|
||||
/>
|
||||
<Phone
|
||||
label={phoneNumber}
|
||||
name="phoneNumber"
|
||||
placeholder={phoneNumber}
|
||||
/>
|
||||
<Select
|
||||
items={languageSelect}
|
||||
@@ -79,12 +85,7 @@ export default function FormContent() {
|
||||
placeholder={password}
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
label={newPassword}
|
||||
name="newPassword"
|
||||
placeholder={newPassword}
|
||||
type="password"
|
||||
/>
|
||||
<NewPassword />
|
||||
<Input
|
||||
label={retypeNewPassword}
|
||||
name="retypeNewPassword"
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useFormState as useReactFormState } from "react-dom"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { profile } from "@/constants/routes/myPages"
|
||||
|
||||
import { editProfile } from "@/actions/editProfile"
|
||||
import Header from "@/components/Profile/Header"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import FormContent from "./FormContent"
|
||||
@@ -19,11 +22,14 @@ import type {
|
||||
EditFormProps,
|
||||
State,
|
||||
} from "@/types/components/myPages/myProfile/edit"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const formId = "edit-profile"
|
||||
|
||||
export default function Form({ user }: EditFormProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const params = useParams()
|
||||
const lang = params.lang as Lang
|
||||
/**
|
||||
* like react, react-hook-form also exports a useFormState hook,
|
||||
* we want to clearly keep them separate by naming.
|
||||
@@ -33,12 +39,14 @@ export default function Form({ user }: EditFormProps) {
|
||||
null
|
||||
)
|
||||
|
||||
const form = useForm<EditProfileSchema>({
|
||||
const methods = useForm<EditProfileSchema>({
|
||||
defaultValues: {
|
||||
"address.city": user.address.city ?? "",
|
||||
"address.countryCode": user.address.countryCode ?? "",
|
||||
"address.streetAddress": user.address.streetAddress ?? "",
|
||||
"address.zipCode": user.address.zipCode ?? "",
|
||||
address: {
|
||||
city: user.address.city ?? "",
|
||||
countryCode: user.address.countryCode ?? "",
|
||||
streetAddress: user.address.streetAddress ?? "",
|
||||
zipCode: user.address.zipCode ?? "",
|
||||
},
|
||||
dateOfBirth: user.dateOfBirth,
|
||||
email: user.email,
|
||||
language: user.language,
|
||||
@@ -53,7 +61,7 @@ export default function Form({ user }: EditFormProps) {
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<>
|
||||
<Header>
|
||||
<hgroup>
|
||||
<Title as="h4" color="red" level="h1">
|
||||
@@ -64,17 +72,15 @@ export default function Form({ user }: EditFormProps) {
|
||||
</Title>
|
||||
</hgroup>
|
||||
<div className={styles.btns}>
|
||||
<Button
|
||||
form={formId}
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
type="reset"
|
||||
>
|
||||
{formatMessage({ id: "Discard changes" })}
|
||||
<Button asChild intent="secondary" size="small" theme="base">
|
||||
<Link href={profile[lang]}>
|
||||
{formatMessage({ id: "Discard changes" })}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!form.formState.isValid || form.formState.isSubmitting}
|
||||
disabled={
|
||||
!methods.formState.isValid || methods.formState.isSubmitting
|
||||
}
|
||||
form={formId}
|
||||
intent="primary"
|
||||
size="small"
|
||||
@@ -85,10 +91,11 @@ export default function Form({ user }: EditFormProps) {
|
||||
</Button>
|
||||
</div>
|
||||
</Header>
|
||||
<Divider color="burgundy" opacity={8} />
|
||||
<form action={formAction} className={styles.form} id={formId}>
|
||||
<FormContent />
|
||||
<FormProvider {...methods}>
|
||||
<FormContent />
|
||||
</FormProvider>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,98 @@
|
||||
import { z } from "zod"
|
||||
|
||||
// import { phoneValidator } from "@/utils/phoneValidator"
|
||||
import { Key } from "@/components/TempDesignSystem/Form/NewPassword/newPassword"
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
export const editProfileSchema = z.object({
|
||||
"address.city": z.string().optional(),
|
||||
"address.countryCode": z.string().min(1),
|
||||
"address.streetAddress": z.string().optional(),
|
||||
"address.zipCode": z.string().min(1),
|
||||
dateOfBirth: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
language: z.string(),
|
||||
phoneNumber: z.string(),
|
||||
// phoneValidator(
|
||||
// "Phone is required",
|
||||
// "Please enter a valid phone number"
|
||||
// ),
|
||||
const countryRequiredMsg = "Country is required"
|
||||
export const editProfileSchema = z
|
||||
.object({
|
||||
address: z.object({
|
||||
city: z.string().optional(),
|
||||
countryCode: z
|
||||
.string({
|
||||
required_error: countryRequiredMsg,
|
||||
invalid_type_error: countryRequiredMsg,
|
||||
})
|
||||
.min(1, countryRequiredMsg),
|
||||
streetAddress: z.string().optional(),
|
||||
zipCode: z.string().min(1, "Zip code is required"),
|
||||
}),
|
||||
dateOfBirth: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
language: z.string(),
|
||||
phoneNumber: phoneValidator(
|
||||
"Phone is required",
|
||||
"Please enter a valid phone number"
|
||||
),
|
||||
|
||||
currentPassword: z.string().optional(),
|
||||
newPassword: z.string().optional(),
|
||||
retypeNewPassword: z.string().optional(),
|
||||
})
|
||||
currentPassword: z.string().optional(),
|
||||
newPassword: z.string().optional(),
|
||||
retypeNewPassword: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.currentPassword) {
|
||||
if (!data.newPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "New password is required",
|
||||
path: ["newPassword"],
|
||||
})
|
||||
}
|
||||
if (!data.retypeNewPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Retype new password is required",
|
||||
path: ["retypeNewPassword"],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (data.newPassword || data.retypeNewPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Current password is required",
|
||||
path: ["currentPassword"],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Retype new password is required",
|
||||
path: ["retypeNewPassword"],
|
||||
})
|
||||
}
|
||||
|
||||
if (data.retypeNewPassword !== data.newPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Retype new password does not match new password",
|
||||
path: ["retypeNewPassword"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type EditProfileSchema = z.infer<typeof editProfileSchema>
|
||||
|
||||
36
components/Icons/Close.tsx
Normal file
36
components/Icons/Close.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function CloseIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
height="20"
|
||||
id="mask0_7531_17458"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style={{ maskType: "alpha" }}
|
||||
width="20"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_7531_17458)">
|
||||
<path
|
||||
d="M10 11.0936L7.58333 13.5207C7.43056 13.6734 7.24653 13.7498 7.03125 13.7498C6.81597 13.7498 6.63194 13.6734 6.47917 13.5207C6.32639 13.3679 6.25 13.1839 6.25 12.9686C6.25 12.7533 6.32639 12.5693 6.47917 12.4165L8.90625 9.99984L6.47917 7.59359C6.32639 7.44081 6.25 7.25678 6.25 7.0415C6.25 6.82623 6.32639 6.6422 6.47917 6.48942C6.63194 6.33664 6.81597 6.26025 7.03125 6.26025C7.24653 6.26025 7.43056 6.33664 7.58333 6.48942L10 8.9165L12.4062 6.48942C12.559 6.33664 12.7431 6.26025 12.9583 6.26025C13.1736 6.26025 13.3576 6.33664 13.5104 6.48942C13.6701 6.64914 13.75 6.83491 13.75 7.04671C13.75 7.25852 13.6701 7.44081 13.5104 7.59359L11.0833 9.99984L13.5104 12.4165C13.6632 12.5693 13.7396 12.7533 13.7396 12.9686C13.7396 13.1839 13.6632 13.3679 13.5104 13.5207C13.3507 13.6804 13.1649 13.7603 12.9531 13.7603C12.7413 13.7603 12.559 13.6804 12.4062 13.5207L10 11.0936Z"
|
||||
fill="#CD0921"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export { default as CheckIcon } from "./Check"
|
||||
export { default as CheckCircleIcon } from "./CheckCircle"
|
||||
export { default as ChevronDownIcon } from "./ChevronDown"
|
||||
export { default as ChevronRightIcon } from "./ChevronRight"
|
||||
export { default as CloseIcon } from "./Close"
|
||||
export { default as EmailIcon } from "./Email"
|
||||
export { default as GlobeIcon } from "./Globe"
|
||||
export { default as HouseIcon } from "./House"
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.comboBoxContainer:has(
|
||||
.input[data-invalid="true"],
|
||||
.input[aria-invalid="true"]
|
||||
) {
|
||||
border-color: var(--Scandic-Red-60);
|
||||
}
|
||||
|
||||
.label {
|
||||
grid-area: label;
|
||||
}
|
||||
@@ -66,6 +73,8 @@
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.listBoxItem[data-focused="true"],
|
||||
.listBoxItem[data-focus-visible="true"],
|
||||
.listBoxItem[data-selected="true"],
|
||||
.listBoxItem:hover {
|
||||
background-color: var(--Scandic-Blue-00);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client"
|
||||
import { ErrorMessage } from "@hookform/error-message"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
FieldError,
|
||||
Input,
|
||||
type Key,
|
||||
ListBox,
|
||||
@@ -18,6 +16,7 @@ import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import SelectChevron from "@/components/TempDesignSystem/Form/SelectChevron"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import ErrorMessage from "../ErrorMessage"
|
||||
import { countries } from "./countries"
|
||||
|
||||
import styles from "./country.module.css"
|
||||
@@ -42,7 +41,7 @@ export default function CountrySelect({
|
||||
}
|
||||
}
|
||||
const { control, setValue } = useFormContext()
|
||||
const { field } = useController({
|
||||
const { field, fieldState, formState } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
@@ -60,6 +59,7 @@ export default function CountrySelect({
|
||||
aria-label={formatMessage({ id: "Select country of residence" })}
|
||||
className={styles.select}
|
||||
isRequired={!!registerOptions?.required}
|
||||
isInvalid={fieldState.invalid}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onSelectionChange={handleChange}
|
||||
@@ -85,9 +85,7 @@ export default function CountrySelect({
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
</div>
|
||||
<FieldError>
|
||||
<ErrorMessage name={name} />
|
||||
</FieldError>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
|
||||
@@ -8,5 +8,5 @@ export const enum DateName {
|
||||
export interface DateProps
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
name: string
|
||||
registerOptions: RegisterOptions
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
"use client"
|
||||
import { parseDate } from "@internationalized/date"
|
||||
import {
|
||||
DateInput,
|
||||
DatePicker,
|
||||
DateSegment,
|
||||
Group,
|
||||
} from "react-aria-components"
|
||||
import { DateInput, DatePicker, Group } from "react-aria-components"
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -23,7 +18,7 @@ import type { Key } from "react-aria-components"
|
||||
import type { DateProps } from "./date"
|
||||
|
||||
/** TODO: Get selecting with Enter-key to work */
|
||||
export default function DateSelect({ name, registerOptions }: DateProps) {
|
||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const d = useWatch({ name })
|
||||
const { control, setValue } = useFormContext()
|
||||
@@ -84,7 +79,7 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DateSegment className={styles.day} segment={segment}>
|
||||
<div className={styles.day}>
|
||||
<Select
|
||||
aria-label={dayLabel}
|
||||
items={days}
|
||||
@@ -92,13 +87,15 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
|
||||
name={DateName.date}
|
||||
onSelect={createOnSelect(DateName.date)}
|
||||
placeholder="DD"
|
||||
required
|
||||
tabIndex={3}
|
||||
value={segment.value}
|
||||
/>
|
||||
</DateSegment>
|
||||
</div>
|
||||
)
|
||||
case "month":
|
||||
return (
|
||||
<DateSegment className={styles.month} segment={segment}>
|
||||
<div className={styles.month}>
|
||||
<Select
|
||||
aria-label={monthLabel}
|
||||
items={months}
|
||||
@@ -106,13 +103,15 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
|
||||
name={DateName.month}
|
||||
onSelect={createOnSelect(DateName.month)}
|
||||
placeholder="MM"
|
||||
required
|
||||
tabIndex={2}
|
||||
value={segment.value}
|
||||
/>
|
||||
</DateSegment>
|
||||
</div>
|
||||
)
|
||||
case "year":
|
||||
return (
|
||||
<DateSegment className={styles.year} segment={segment}>
|
||||
<div className={styles.year}>
|
||||
<Select
|
||||
aria-label={yearLabel}
|
||||
items={years}
|
||||
@@ -120,9 +119,11 @@ export default function DateSelect({ name, registerOptions }: DateProps) {
|
||||
name={DateName.year}
|
||||
onSelect={createOnSelect(DateName.year)}
|
||||
placeholder="YYYY"
|
||||
required
|
||||
tabIndex={1}
|
||||
value={segment.value}
|
||||
/>
|
||||
</DateSegment>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
/** DateInput forces return of ReactElement */
|
||||
|
||||
13
components/TempDesignSystem/Form/ErrorMessage/Error.tsx
Normal file
13
components/TempDesignSystem/Form/ErrorMessage/Error.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
|
||||
export default function Error({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<Caption className={styles.message} fontOnly>
|
||||
<InfoCircleIcon color="red" />
|
||||
{children}
|
||||
</Caption>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
import Error from "./Error"
|
||||
|
||||
import type { ErrorMessageProps } from "./errorMessage"
|
||||
|
||||
@@ -15,12 +12,7 @@ export default function ErrorMessage<T>({
|
||||
<RHFErrorMessage
|
||||
errors={errors}
|
||||
name={name}
|
||||
render={({ message }) => (
|
||||
<Caption className={styles.message} fontOnly>
|
||||
<InfoCircleIcon color="red" />
|
||||
{message}
|
||||
</Caption>
|
||||
)}
|
||||
render={({ message }) => <Error>{message}</Error>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,66 +2,86 @@
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
Text,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
import { Controller, useFormContext } from "react-hook-form"
|
||||
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./input.module.css"
|
||||
|
||||
import type { HTMLAttributes, WheelEvent } from "react"
|
||||
|
||||
import type { InputProps } from "./input"
|
||||
|
||||
export default function Input({
|
||||
"aria-label": ariaLabel,
|
||||
disabled,
|
||||
disabled = false,
|
||||
helpText = "",
|
||||
label,
|
||||
name,
|
||||
placeholder = "",
|
||||
registerOptions = {},
|
||||
required = false,
|
||||
type = "text",
|
||||
}: InputProps) {
|
||||
const { control } = useFormContext()
|
||||
const rules = {
|
||||
...registerOptions,
|
||||
required:
|
||||
"required" in registerOptions ? !!registerOptions.required : required,
|
||||
let numberAttributes: HTMLAttributes<HTMLInputElement> = {}
|
||||
if (type === "number") {
|
||||
numberAttributes.onWheel = function (evt: WheelEvent<HTMLInputElement>) {
|
||||
evt.currentTarget.blur()
|
||||
}
|
||||
}
|
||||
const { field, fieldState, formState } = useController({
|
||||
control,
|
||||
name,
|
||||
rules,
|
||||
})
|
||||
|
||||
return (
|
||||
<TextField
|
||||
aria-label={ariaLabel}
|
||||
defaultValue={field.value}
|
||||
isDisabled={disabled ?? field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions?.required}
|
||||
name={field.name}
|
||||
type={type}
|
||||
>
|
||||
<AriaLabel className={styles.container} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
required={rules.required}
|
||||
/>
|
||||
</Body>
|
||||
<Label required={rules.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
<ErrorMessage errors={formState.errors} name={field.name} />
|
||||
</TextField>
|
||||
<Controller
|
||||
disabled={disabled}
|
||||
control={control}
|
||||
name={name}
|
||||
rules={registerOptions}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
aria-label={ariaLabel}
|
||||
isDisabled={field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions.required}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
validationBehavior="aria"
|
||||
value={field.value}
|
||||
>
|
||||
<AriaLabel className={styles.container} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
{...numberAttributes}
|
||||
aria-labelledby={field.name}
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!registerOptions.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
{helpText && !fieldState.error ? (
|
||||
<Caption asChild color="black">
|
||||
<Text className={styles.helpText} slot="description">
|
||||
<CheckIcon height={20} width={30} />
|
||||
{helpText}
|
||||
</Text>
|
||||
</Caption>
|
||||
) : null}
|
||||
{fieldState.error ? (
|
||||
<Caption className={styles.error} fontOnly>
|
||||
<InfoCircleIcon color="red" />
|
||||
{fieldState.error.message}
|
||||
</Caption>
|
||||
) : null}
|
||||
</TextField>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@
|
||||
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;
|
||||
@@ -42,3 +52,21 @@
|
||||
height: 18px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.helpText {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
import type { RegisterOptions, UseFormRegister } from "react-hook-form"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
helpText?: string
|
||||
label: string
|
||||
name: string
|
||||
registerOptions?: RegisterOptions
|
||||
|
||||
@@ -28,3 +28,7 @@ input:placeholder-shown ~ .label {
|
||||
align-self: center;
|
||||
grid-row: 1/-1;
|
||||
}
|
||||
|
||||
input:disabled ~ .label {
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
113
components/TempDesignSystem/Form/NewPassword/index.tsx
Normal file
113
components/TempDesignSystem/Form/NewPassword/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client"
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
Text,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { Controller, useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CheckIcon, CloseIcon } from "@/components/Icons"
|
||||
import Error from "@/components/TempDesignSystem/Form/ErrorMessage/Error"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import { type IconProps, Key, type NewPasswordProps } from "./newPassword"
|
||||
|
||||
import styles from "./newPassword.module.css"
|
||||
|
||||
export default function NewPassword({
|
||||
"aria-label": ariaLabel,
|
||||
disabled = false,
|
||||
placeholder = "",
|
||||
registerOptions = {},
|
||||
}: NewPasswordProps) {
|
||||
const { control } = useFormContext()
|
||||
const { formatMessage } = useIntl()
|
||||
return (
|
||||
<Controller
|
||||
disabled={disabled}
|
||||
control={control}
|
||||
name="newPassword"
|
||||
rules={registerOptions}
|
||||
render={({ field, fieldState }) => {
|
||||
const messages = (fieldState.error?.message?.split(",") ?? []) as Key[]
|
||||
return (
|
||||
<TextField
|
||||
aria-label={ariaLabel}
|
||||
isDisabled={field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions.required}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
validationBehavior="aria"
|
||||
value={field.value}
|
||||
type="password"
|
||||
>
|
||||
<AriaLabel className={styles.container} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
aria-labelledby={field.name}
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
placeholder={placeholder}
|
||||
type="password"
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!registerOptions.required}>
|
||||
{formatMessage({ id: "New password" })}
|
||||
</Label>
|
||||
</AriaLabel>
|
||||
{field.value ? (
|
||||
<div className={styles.errors}>
|
||||
<Caption asChild color="black">
|
||||
<Text className={styles.helpText} slot="description">
|
||||
<Icon matcher={Key.CHAR_LENGTH} messages={messages} />
|
||||
10 {formatMessage({ id: "to" })} 40{" "}
|
||||
{formatMessage({ id: "characters" })}
|
||||
</Text>
|
||||
</Caption>
|
||||
<Caption asChild color="black">
|
||||
<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>
|
||||
) : null}
|
||||
{!field.value && fieldState.error ? (
|
||||
<Error>
|
||||
<Text className={styles.helpText} slot="description">
|
||||
{fieldState.error.message}
|
||||
</Text>
|
||||
</Error>
|
||||
) : null}
|
||||
</TextField>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Icon({ matcher, messages }: IconProps) {
|
||||
return messages.includes(matcher) ? (
|
||||
<CloseIcon color="red" height={20} width={20} />
|
||||
) : (
|
||||
<CheckIcon color="green" height={20} width={20} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
.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;
|
||||
gap: var(--Spacing-x-half);
|
||||
grid-template-rows: auto auto;
|
||||
height: 60px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.container:has(.input:not(:focus):placeholder-shown) {
|
||||
gap: 0;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus:placeholder-shown {
|
||||
height: 18px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
color: var(--Main-Grey-40);
|
||||
}
|
||||
|
||||
.errors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||
padding-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.helpText {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
19
components/TempDesignSystem/Form/NewPassword/newPassword.ts
Normal file
19
components/TempDesignSystem/Form/NewPassword/newPassword.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
matcher: Key
|
||||
messages: Key[]
|
||||
}
|
||||
@@ -132,7 +132,6 @@ export default function Phone({
|
||||
</AriaLabel>
|
||||
<ErrorMessage errors={formState.errors} name={field.name} />
|
||||
</TextField>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.inputContainer:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||
border-color: var(--Scandic-Red-60);
|
||||
}
|
||||
|
||||
.input {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -31,6 +31,8 @@ export default function Select({
|
||||
name,
|
||||
onSelect,
|
||||
placeholder,
|
||||
required = false,
|
||||
tabIndex,
|
||||
value,
|
||||
}: SelectProps) {
|
||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||
@@ -58,8 +60,10 @@ export default function Select({
|
||||
>
|
||||
<Body asChild fontOnly>
|
||||
<Button className={styles.input}>
|
||||
<div className={styles.inputContentWrapper}>
|
||||
<Label size="small">{label}</Label>
|
||||
<div className={styles.inputContentWrapper} tabIndex={tabIndex}>
|
||||
<Label required={required} size="small">
|
||||
{label}
|
||||
</Label>
|
||||
<SelectValue />
|
||||
</div>
|
||||
<SelectChevron />
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.listBoxItem[data-focused="true"] {
|
||||
.listBoxItem[data-focused="true"],
|
||||
.listBoxItem[data-selected="true"] {
|
||||
background: var(--UI-Input-Controls-Surface-Hover, var(--Scandic-Blue-00));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Key } from "react-aria-components"
|
||||
import type { Key, SelectProps as AriaSelectProps } from "react-aria-components"
|
||||
|
||||
export interface SelectProps
|
||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Book": "Bestil",
|
||||
"Booking number": "Bestillingsnummer",
|
||||
"Cancel": "Afbestille",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.",
|
||||
"City": "By",
|
||||
"City/State": "By/Stat",
|
||||
"Click here to log in": "Klik her for at logge ind",
|
||||
@@ -21,6 +22,7 @@
|
||||
"Country code": "Landekode",
|
||||
"Current level": "Nuværende niveau",
|
||||
"Current password": "Nuværende kodeord",
|
||||
"characters": "tegn",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivelse",
|
||||
@@ -45,6 +47,7 @@
|
||||
"Month": "Måned",
|
||||
"My communication preferences": "Mine kommunikationspræferencer",
|
||||
"My credit cards": "Mine kreditkort",
|
||||
"My membership cards": "Mine medlemskort",
|
||||
"My pages": "Mine sider",
|
||||
"My wishes": "Mine ønsker",
|
||||
"New password": "Nyt kodeord",
|
||||
@@ -55,6 +58,7 @@
|
||||
"Not found": "Ikke fundet",
|
||||
"night": "nat",
|
||||
"nights": "nætter",
|
||||
"number": "nummer",
|
||||
"On your journey": "På din rejse",
|
||||
"Open": "Åben",
|
||||
"or": "eller",
|
||||
@@ -79,10 +83,13 @@
|
||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||
"Something went wrong!": "Noget gik galt!",
|
||||
"Street": "Gade",
|
||||
"special character": "speciel karakter",
|
||||
"Total Points": "Samlet antal point",
|
||||
"Transaction date": "Overførselsdato",
|
||||
"Transactions": "Transaktioner",
|
||||
"to": "til",
|
||||
"User information": "Brugeroplysninger",
|
||||
"uppercase letter": "stort bogstav",
|
||||
"Visiting address": "Besøgsadresse",
|
||||
"Where should you go next?": "Hvor skal du tage hen næste gang?",
|
||||
"Year": "År",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Book": "Buch",
|
||||
"Booking number": "Buchungsnummer",
|
||||
"Cancel": "Stornieren",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.",
|
||||
"City": "Stadt",
|
||||
"City/State": "Stadt/Zustand",
|
||||
"Click here to log in": "Klicken Sie hier, um sich einzuloggen",
|
||||
@@ -21,6 +22,7 @@
|
||||
"Country code": "Landesvorwahl",
|
||||
"Current level": "Aktuelles Level",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"characters": "figuren",
|
||||
"Date of Birth": "Geburtsdatum",
|
||||
"Day": "Tag",
|
||||
"Description": "Beschreibung",
|
||||
@@ -45,6 +47,7 @@
|
||||
"Month": "Monat",
|
||||
"My communication preferences": "Meine Kommunikationseinstellungen",
|
||||
"My credit cards": "Meine Kreditkarten",
|
||||
"My membership cards": "Meine Mitgliedskarten",
|
||||
"My pages": "Meine Seiten",
|
||||
"My wishes": "Meine Wünsche",
|
||||
"New password": "Neues Kennwort",
|
||||
@@ -55,6 +58,7 @@
|
||||
"Not found": "Nicht gefunden",
|
||||
"night": "nacht",
|
||||
"nights": "nächte",
|
||||
"number": "nummer",
|
||||
"On your journey": "Auf deiner Reise",
|
||||
"Open": "Offen",
|
||||
"or": "oder",
|
||||
@@ -79,10 +83,13 @@
|
||||
"Skip to main content": "Direkt zum Inhalt",
|
||||
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||
"Street": "Straße",
|
||||
"special character": "sonderzeichen",
|
||||
"Total Points": "Gesamtpunktzahl",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktionen",
|
||||
"to": "zu",
|
||||
"User information": "Nutzerinformation",
|
||||
"uppercase letter": "großbuchstabe",
|
||||
"Visiting address": "Besuchsadresse",
|
||||
"Where should you go next?": "Wohin soll es als nächstes gehen?",
|
||||
"Year": "Jahr",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Book": "Book",
|
||||
"Booking number": "Booking number",
|
||||
"Cancel": "Cancel",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
|
||||
"City": "City",
|
||||
"City/State": "City/State",
|
||||
"Click here to log in": "Click here to log in",
|
||||
@@ -21,6 +22,7 @@
|
||||
"Country code": "Country code",
|
||||
"Current level": "Current level",
|
||||
"Current password": "Current password",
|
||||
"characters": "characters",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Day": "Day",
|
||||
"Description": "Description",
|
||||
@@ -45,6 +47,7 @@
|
||||
"Month": "Month",
|
||||
"My communication preferences": "My communication preferences",
|
||||
"My credit cards": "My credit cards",
|
||||
"My membership cards": "My membership cards",
|
||||
"My pages": "My pages",
|
||||
"My wishes": "My wishes",
|
||||
"New password": "New password",
|
||||
@@ -55,6 +58,7 @@
|
||||
"Not found": "Not found",
|
||||
"night": "night",
|
||||
"nights": "nights",
|
||||
"number": "number",
|
||||
"On your journey": "On your journey",
|
||||
"Open": "Open",
|
||||
"or": "or",
|
||||
@@ -79,10 +83,13 @@
|
||||
"Skip to main content": "Skip to main content",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"Street": "Street",
|
||||
"special character": "special character",
|
||||
"Total Points": "Total Points",
|
||||
"Transaction date": "Transaction date",
|
||||
"Transactions": "Transactions",
|
||||
"to": "to",
|
||||
"User information": "User information",
|
||||
"uppercase letter": "uppercase letter",
|
||||
"Visiting address": "Visiting address",
|
||||
"Where should you go next?": "Where should you go next?",
|
||||
"Year": "Year",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Book": "Kirja",
|
||||
"Booking number": "Varausnumero",
|
||||
"Cancel": "Peruuttaa",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.",
|
||||
"City": "Kaupunki",
|
||||
"City/State": "Kaupunki/Osavaltio",
|
||||
"Click here to log in": "Napsauta tästä kirjautuaksesi sisään",
|
||||
@@ -21,6 +22,7 @@
|
||||
"Country code": "Maatunnus",
|
||||
"Current level": "Nykyinen taso",
|
||||
"Current password": "Nykyinen salasana",
|
||||
"characters": "hahmoja",
|
||||
"Date of Birth": "Syntymäaika",
|
||||
"Day": "Päivä",
|
||||
"Description": "Kuvaus",
|
||||
@@ -45,6 +47,7 @@
|
||||
"Month": "Kuukausi",
|
||||
"My communication preferences": "Viestintämieltymykseni",
|
||||
"My credit cards": "Minun luottokorttini",
|
||||
"My membership cards": "Jäsenkorttini",
|
||||
"My pages": "Omat sivut",
|
||||
"My wishes": "Toiveeni",
|
||||
"New password": "Uusi salasana",
|
||||
@@ -55,6 +58,7 @@
|
||||
"Not found": "Ei löydetty",
|
||||
"night": "yö",
|
||||
"nights": "yöt",
|
||||
"number": "määrä",
|
||||
"On your journey": "Matkallasi",
|
||||
"Open": "Avata",
|
||||
"or": "tai",
|
||||
@@ -79,10 +83,13 @@
|
||||
"Skip to main content": "Siirry pääsisältöön",
|
||||
"Something went wrong!": "Jotain meni pieleen!",
|
||||
"Street": "Katu",
|
||||
"special character": "erikoishahmo",
|
||||
"Total Points": "Kokonaispisteet",
|
||||
"Transaction date": "Tapahtuman päivämäärä",
|
||||
"Transactions": "Tapahtumat",
|
||||
"to": "to",
|
||||
"User information": "Käyttäjän tiedot",
|
||||
"uppercase letter": "iso kirjain",
|
||||
"Visiting address": "Käyntiosoite",
|
||||
"Where should you go next?": "Minne sinun pitäisi mennä seuraavaksi?",
|
||||
"Year": "Vuosi",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Book": "Bok",
|
||||
"Booking number": "Bestillingsnummer",
|
||||
"Cancel": "Avbryt",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.",
|
||||
"City": "By",
|
||||
"City/State": "By/Stat",
|
||||
"Click here to log in": "Klikk her for å logge inn",
|
||||
@@ -21,6 +22,7 @@
|
||||
"Country code": "Landskode",
|
||||
"Current level": "Nåværende nivå",
|
||||
"Current password": "Nåværende passord",
|
||||
"characters": "tegn",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivelse",
|
||||
@@ -45,6 +47,7 @@
|
||||
"Month": "Måned",
|
||||
"My communication preferences": "Mine kommunikasjonspreferanser",
|
||||
"My credit cards": "Kredittkortene mine",
|
||||
"My membership cards": "Mine medlemskort",
|
||||
"My pages": "Mine sider",
|
||||
"My wishes": "Mine ønsker",
|
||||
"New password": "Nytt passord",
|
||||
@@ -55,6 +58,7 @@
|
||||
"Not found": "Ikke funnet",
|
||||
"night": "natt",
|
||||
"nights": "netter",
|
||||
"number": "antall",
|
||||
"On your journey": "På reisen din",
|
||||
"Open": "Åpen",
|
||||
"or": "eller",
|
||||
@@ -79,10 +83,13 @@
|
||||
"Skip to main content": "Gå videre til hovedsiden",
|
||||
"Something went wrong!": "Noe gikk galt!",
|
||||
"Street": "Gate",
|
||||
"special character": "spesiell karakter",
|
||||
"Total Points": "Totale poeng",
|
||||
"Transaction date": "Transaksjonsdato",
|
||||
"Transactions": "Transaksjoner",
|
||||
"to": "til",
|
||||
"User information": "Brukerinformasjon",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Visiting address": "Besøksadresse",
|
||||
"Where should you go next?": "Hvor bør du gå videre?",
|
||||
"Year": "År",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Book": "Boka",
|
||||
"Booking number": "Bokningsnummer",
|
||||
"Cancel": "Avbryt",
|
||||
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.",
|
||||
"City": "Ort",
|
||||
"City/State": "Ort",
|
||||
"Click here to log in": "Klicka här för att logga in",
|
||||
@@ -21,6 +22,7 @@
|
||||
"Country code": "Landskod",
|
||||
"Current level": "Nuvarande nivå",
|
||||
"Current password": "Nuvarande lösenord",
|
||||
"characters": "tecken",
|
||||
"Date of Birth": "Födelsedatum",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivning",
|
||||
@@ -45,6 +47,7 @@
|
||||
"Month": "Månad",
|
||||
"My communication preferences": "Mina kommunikationspreferenser",
|
||||
"My credit cards": "Mina kreditkort",
|
||||
"My membership cards": "Mina medlemskort",
|
||||
"My pages": "Mina sidor",
|
||||
"My wishes": "Mina önskningar",
|
||||
"New password": "Nytt lösenord",
|
||||
@@ -55,6 +58,7 @@
|
||||
"Not found": "Hittades inte",
|
||||
"night": "natt",
|
||||
"nights": "nätter",
|
||||
"number": "nummer",
|
||||
"On your journey": "På din resa",
|
||||
"Open": "Öppna",
|
||||
"or": "eller",
|
||||
@@ -79,10 +83,13 @@
|
||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||
"Something went wrong!": "Något gick fel!",
|
||||
"Street": "Gata",
|
||||
"special character": "speciell karaktär",
|
||||
"Total Points": "Total poäng",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktioner",
|
||||
"to": "till",
|
||||
"User information": "Användar information",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Visiting address": "Besöksadress",
|
||||
"Where should you go next?": "Vart ska du gå härnäst?",
|
||||
"Year": "År",
|
||||
|
||||
Reference in New Issue
Block a user