feat(WEB-162): final design edit profile page
This commit is contained in:
committed by
Christel Westerberg
parent
5f3e417593
commit
d84efcbb0f
@@ -1,13 +1,13 @@
|
||||
"use server"
|
||||
|
||||
import { editProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||
// import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { type State, Status } from "@/types/components/myPages/myProfile/edit"
|
||||
|
||||
export async function editProfile(_prevState: State, values: FormData) {
|
||||
try {
|
||||
const data = editProfileSchema.parse(Object.fromEntries(values.entries()))
|
||||
const data = Object.fromEntries(values.entries())
|
||||
|
||||
/**
|
||||
* TODO: Update profile data when endpoint from
|
||||
|
||||
@@ -11,19 +11,19 @@ export default async function CommunicationSlot() {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<article className={styles.content}>
|
||||
<Subtitle>
|
||||
<Subtitle color="black">
|
||||
{formatMessage({ id: "My communication preferences" })}
|
||||
</Subtitle>
|
||||
<Body>
|
||||
<Body color="black">
|
||||
{formatMessage({
|
||||
id: "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
|
||||
})}
|
||||
</Body>
|
||||
</article>
|
||||
<Link href="#" variant="icon">
|
||||
<ArrowRightIcon color="red" />
|
||||
<Body color="red" textTransform="underlined">
|
||||
{formatMessage({ id: "Add new card" })}
|
||||
<ArrowRightIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="underlined">
|
||||
{formatMessage({ id: "Manage preferences" })}
|
||||
</Body>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
@@ -11,16 +11,18 @@ export default async function CreditCardSlot() {
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<article className={styles.content}>
|
||||
<Subtitle>{formatMessage({ id: "My credit cards" })}</Subtitle>
|
||||
<Body>
|
||||
<Subtitle color="black">
|
||||
{formatMessage({ id: "My credit cards" })}
|
||||
</Subtitle>
|
||||
<Body color="black">
|
||||
{formatMessage({
|
||||
id: "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
|
||||
})}
|
||||
</Body>
|
||||
</article>
|
||||
<Link href="#" variant="icon">
|
||||
<PlusCircleIcon color="red" />
|
||||
<Body color="red" textTransform="underlined">
|
||||
<PlusCircleIcon color="burgundy" />
|
||||
<Body color="burgundy" textTransform="underlined">
|
||||
{formatMessage({ id: "Add new card" })}
|
||||
</Body>
|
||||
</Link>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function Default() {
|
||||
export default function DefaultEditProfileSlot() {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import EditProfile from "@/components/MyProfile/Profile/Edit"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
export default function EditProfileSlot() {
|
||||
return <EditProfile />
|
||||
import Form from "@/components/Forms/Edit/Profile"
|
||||
|
||||
export default async function EditProfileSlot() {
|
||||
const user = await serverClient().user.get({ mask: false })
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return <Form user={user} />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.container {
|
||||
background-color: var(--Scandic-Brand-Pale-Peach);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
export default function ProfileSlotLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren) {
|
||||
return <section className={styles.container}>{children}</section>
|
||||
}
|
||||
@@ -1,22 +1,3 @@
|
||||
.container {
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button.btn {
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// import { dt } from "@/lib/dt"
|
||||
import { profileEdit } from "@/constants/routes/myPages"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import {
|
||||
@@ -8,36 +8,41 @@ import {
|
||||
LockIcon,
|
||||
PhoneIcon,
|
||||
} from "@/components/Icons"
|
||||
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 Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
export default async function Profile() {
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||
const { formatMessage } = await getIntl()
|
||||
const user = await serverClient().user.get()
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
// const dob = dt(user.dateOfBirth).format("DD/MM/YYYY")
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<header className={styles.header}>
|
||||
<>
|
||||
<Header>
|
||||
<hgroup>
|
||||
<Title as="h4" color="red" level="h1">
|
||||
{user.name}
|
||||
<Title as="h4" color="peach80" level="h1">
|
||||
{formatMessage({ id: "Welcome" })}
|
||||
</Title>
|
||||
<Title as="h4" color="burgundy" level="h2">
|
||||
{user.dateOfBirth}
|
||||
{user.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<Button className={styles.btn} size="large" theme="primaryStrong">
|
||||
{formatMessage({ id: "Edit profile" })}
|
||||
<Button asChild intent="secondary" size="small" theme="base">
|
||||
<Link color="none" href={profileEdit[params.lang]}>
|
||||
{formatMessage({ id: "Edit profile" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</header>
|
||||
</Header>
|
||||
<Divider color="burgundy" opacity={8} />
|
||||
<section className={styles.profile}>
|
||||
<article className={styles.info}>
|
||||
@@ -91,6 +96,6 @@ export default async function Profile() {
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import "../profileLayout.css"
|
||||
|
||||
export default function EditProfilePage() {
|
||||
return null
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import Desktop from "@/components/Current/Header/LanguageSwitcher/Desktop"
|
||||
import Mobile from "@/components/Current/Header/LanguageSwitcher/Mobile"
|
||||
|
||||
export default async function LanguageSwitcher() {
|
||||
const data = await serverClient().contentstack.languageSwitcher.get()
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Desktop currentLanguage={data.lang} urls={data.urls} />
|
||||
<Mobile currentLanguage={data.lang} urls={data.urls} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function DefaultLanguageSwitcher() {
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +1 @@
|
||||
export { GET, POST } from "@/auth"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
@@ -72,14 +72,6 @@
|
||||
src: url(/_static/fonts/fira-sans/medium.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: "fira sans";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url(/_static/fonts/fira-sans/Medium.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: "fira sans";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.password,
|
||||
.user {
|
||||
align-self: flex-start;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: max(164px) 1fr;
|
||||
}
|
||||
104
components/Forms/Edit/Profile/FormContent/index.tsx
Normal file
104
components/Forms/Edit/Profile/FormContent/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
// import { useFormStatus } from "react-dom"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./formContent.module.css"
|
||||
|
||||
const languages = [
|
||||
{ label: "Danish", value: "Da" },
|
||||
{ label: "German", value: "De" },
|
||||
{ label: "English", value: "En" },
|
||||
{ label: "Finnish", value: "Fi" },
|
||||
{ label: "Norwegian", value: "No" },
|
||||
{ label: "Swedish", value: "Sv" },
|
||||
]
|
||||
|
||||
export default function FormContent() {
|
||||
const { formatMessage } = useIntl()
|
||||
// const { pending } = useFormStatus()
|
||||
|
||||
const city = formatMessage({ id: "City" })
|
||||
const country = formatMessage({ id: "Country" })
|
||||
const email = `${formatMessage({ id: "Email" })} ${formatMessage({ id: "Address" }).toLowerCase()}`
|
||||
const street = formatMessage({ id: "Address" })
|
||||
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" })
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={styles.user}>
|
||||
<header>
|
||||
<Body textTransform="bold">
|
||||
{formatMessage({ id: "User information" })}
|
||||
</Body>
|
||||
</header>
|
||||
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
|
||||
<Input
|
||||
label={`${street} 1`}
|
||||
name="address.streetAddress"
|
||||
placeholder={street}
|
||||
/>
|
||||
<Input label={city} name="address.city" placeholder={city} />
|
||||
<div className={styles.container}>
|
||||
<Input
|
||||
label={zipCode}
|
||||
name="address.zipCode"
|
||||
placeholder={zipCode}
|
||||
required
|
||||
/>
|
||||
<CountrySelect
|
||||
label={country}
|
||||
name="address.countryCode"
|
||||
placeholder={country}
|
||||
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" })}
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Select
|
||||
items={languages}
|
||||
label={formatMessage({ id: "Language" })}
|
||||
name="language"
|
||||
placeholder={formatMessage({ id: "Select language" })}
|
||||
/>
|
||||
</section>
|
||||
<section className={styles.password}>
|
||||
<header>
|
||||
<Body textTransform="bold">{formatMessage({ id: "Password" })}</Body>
|
||||
</header>
|
||||
<Input
|
||||
label={password}
|
||||
name="currentPassword"
|
||||
placeholder={password}
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
label={newPassword}
|
||||
name="newPassword"
|
||||
placeholder={newPassword}
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
label={retypeNewPassword}
|
||||
name="retypeNewPassword"
|
||||
placeholder={retypeNewPassword}
|
||||
type="password"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
10
components/Forms/Edit/Profile/form.module.css
Normal file
10
components/Forms/Edit/Profile/form.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x5);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
94
components/Forms/Edit/Profile/index.tsx
Normal file
94
components/Forms/Edit/Profile/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useFormState as useReactFormState } from "react-dom"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { editProfile } from "@/actions/editProfile"
|
||||
import Header from "@/components/Profile/Header"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import FormContent from "./FormContent"
|
||||
import { type EditProfileSchema, editProfileSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
import type {
|
||||
EditFormProps,
|
||||
State,
|
||||
} from "@/types/components/myPages/myProfile/edit"
|
||||
|
||||
const formId = "edit-profile"
|
||||
|
||||
export default function Form({ user }: EditFormProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
/**
|
||||
* like react, react-hook-form also exports a useFormState hook,
|
||||
* we want to clearly keep them separate by naming.
|
||||
*/
|
||||
const [state, formAction] = useReactFormState<State, FormData>(
|
||||
editProfile,
|
||||
null
|
||||
)
|
||||
|
||||
const form = useForm<EditProfileSchema>({
|
||||
defaultValues: {
|
||||
"address.city": user.address.city ?? "",
|
||||
"address.countryCode": user.address.countryCode ?? "",
|
||||
"address.streetAddress": user.address.streetAddress ?? "",
|
||||
"address.zipCode": user.address.zipCode ?? "",
|
||||
dateOfBirth: user.dateOfBirth,
|
||||
email: user.email,
|
||||
language: user.language,
|
||||
phoneNumber: user.phoneNumber,
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
retypeNewPassword: "",
|
||||
},
|
||||
mode: "all",
|
||||
resolver: zodResolver(editProfileSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Header>
|
||||
<hgroup>
|
||||
<Title as="h4" color="peach80" level="h1">
|
||||
{formatMessage({ id: "Edit" })}
|
||||
</Title>
|
||||
<Title as="h4" color="burgundy" level="h2">
|
||||
{user.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<div className={styles.btns}>
|
||||
<Button
|
||||
form={formId}
|
||||
intent="primary"
|
||||
size="small"
|
||||
theme="primaryDark"
|
||||
type="reset"
|
||||
>
|
||||
{formatMessage({ id: "Discard changes" })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!form.formState.isValid || form.formState.isSubmitting}
|
||||
form={formId}
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
type="submit"
|
||||
>
|
||||
{formatMessage({ id: "Save" })}
|
||||
</Button>
|
||||
</div>
|
||||
</Header>
|
||||
<Divider color="burgundy" opacity={8} />
|
||||
<form action={formAction} className={styles.form} id={formId}>
|
||||
<FormContent />
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
24
components/Forms/Edit/Profile/schema.ts
Normal file
24
components/Forms/Edit/Profile/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod"
|
||||
|
||||
// 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"
|
||||
// ),
|
||||
|
||||
currentPassword: z.string().optional(),
|
||||
newPassword: z.string().optional(),
|
||||
retypeNewPassword: z.string().optional(),
|
||||
})
|
||||
|
||||
export type EditProfileSchema = z.infer<typeof editProfileSchema>
|
||||
@@ -12,27 +12,27 @@ export default function ChevronDownIcon({
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
height="24"
|
||||
id="mask0_553_6963"
|
||||
height="20"
|
||||
id="mask0_4971_13121"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style={{ maskType: "alpha" }}
|
||||
width="24"
|
||||
width="20"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_553_6963)">
|
||||
<g mask="url(#mask0_4971_13121)">
|
||||
<path
|
||||
d="M12 15.3746L6 9.37461L7.4 7.97461L12 12.5746L16.6 7.97461L18 9.37461L12 15.3746Z"
|
||||
fill="#757575"
|
||||
d="M10.422 11.9374L16.2449 6.1145C16.4254 5.93395 16.6459 5.84193 16.9063 5.83846C17.1668 5.83499 17.3873 5.92353 17.5678 6.10409C17.7484 6.28464 17.8386 6.50513 17.8386 6.76554C17.8386 7.02596 17.7484 7.24645 17.5678 7.427L11.4011 13.6041C11.2553 13.7499 11.1025 13.8523 10.9428 13.9114C10.7831 13.9704 10.6095 13.9999 10.422 13.9999C10.2345 13.9999 10.0609 13.9704 9.90114 13.9114C9.74142 13.8523 9.58864 13.7499 9.44281 13.6041L3.27614 7.43742C3.09558 7.25686 3.00357 7.03464 3.0001 6.77075C2.99663 6.50686 3.08517 6.28464 3.26572 6.10409C3.44628 5.92353 3.66676 5.83325 3.92718 5.83325C4.1876 5.83325 4.40808 5.92353 4.58864 6.10409L10.422 11.9374Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
40
components/Icons/InfoCircle.tsx
Normal file
40
components/Icons/InfoCircle.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function InfoCircleIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
height="17"
|
||||
id="mask0_954_4761"
|
||||
maskUnits="userSpaceOnUse"
|
||||
style={{ maskType: "alpha" }}
|
||||
width="16"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<rect y="0.5" width="16" height="16" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_954_4761)">
|
||||
<path
|
||||
d="M8.00833 11.75C8.18056 11.75 8.32778 11.6889 8.45 11.5667C8.57222 11.4444 8.63333 11.2972 8.63333 11.125V8.475C8.63333 8.30278 8.57222 8.15556 8.45 8.03333C8.32778 7.91111 8.18056 7.85 8.00833 7.85C7.83611 7.85 7.68889 7.91111 7.56667 8.03333C7.44444 8.15556 7.38333 8.30278 7.38333 8.475V11.125C7.38333 11.2972 7.44444 11.4444 7.56667 11.5667C7.68889 11.6889 7.83611 11.75 8.00833 11.75ZM7.99825 6.56667C8.17719 6.56667 8.32778 6.50614 8.45 6.38508C8.57222 6.26404 8.63333 6.11404 8.63333 5.93508C8.63333 5.75614 8.57281 5.60556 8.45175 5.48333C8.33071 5.36111 8.18071 5.3 8.00175 5.3C7.82281 5.3 7.67222 5.36053 7.55 5.48158C7.42778 5.60263 7.36667 5.75263 7.36667 5.93158C7.36667 6.11053 7.42719 6.26111 7.54825 6.38333C7.66929 6.50556 7.81929 6.56667 7.99825 6.56667ZM8 15C7.10103 15 6.25623 14.8291 5.46558 14.4873C4.67493 14.1455 3.98717 13.6816 3.4023 13.0956C2.81743 12.5097 2.35417 11.8217 2.0125 11.0319C1.67083 10.242 1.5 9.39806 1.5 8.5C1.5 7.60103 1.67091 6.75623 2.01272 5.96558C2.35453 5.17493 2.81842 4.48717 3.40438 3.9023C3.99035 3.31743 4.67826 2.85417 5.46812 2.5125C6.25798 2.17083 7.10194 2 8 2C8.89897 2 9.74377 2.17091 10.5344 2.51272C11.3251 2.85453 12.0128 3.31842 12.5977 3.90438C13.1826 4.49035 13.6458 5.17826 13.9875 5.96812C14.3292 6.75798 14.5 7.60194 14.5 8.5C14.5 9.39897 14.3291 10.2438 13.9873 11.0344C13.6455 11.8251 13.1816 12.5128 12.5956 13.0977C12.0097 13.6826 11.3217 14.1458 10.5319 14.4875C9.74202 14.8292 8.89806 15 8 15Z"
|
||||
fill="#CD0921"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
.icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.black,
|
||||
@@ -13,6 +12,11 @@
|
||||
fill: var(--Scandic-Brand-Burgundy);
|
||||
}
|
||||
|
||||
.grey80,
|
||||
.grey80 * {
|
||||
fill: var(--UI-Grey-80);
|
||||
}
|
||||
|
||||
.pale,
|
||||
.pale * {
|
||||
fill: var(--Scandic-Brand-Pale-Peach);
|
||||
|
||||
@@ -9,6 +9,7 @@ export { default as ChevronRightIcon } from "./ChevronRight"
|
||||
export { default as EmailIcon } from "./Email"
|
||||
export { default as GlobeIcon } from "./Globe"
|
||||
export { default as HouseIcon } from "./House"
|
||||
export { default as InfoCircleIcon } from "./InfoCircle"
|
||||
export { default as LocationIcon } from "./Location"
|
||||
export { default as LockIcon } from "./Lock"
|
||||
export { default as PhoneIcon } from "./Phone"
|
||||
|
||||
@@ -7,6 +7,7 @@ const config = {
|
||||
color: {
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
grey80: styles.grey80,
|
||||
pale: styles.pale,
|
||||
peach80: styles.peach80,
|
||||
primaryLightOnSurfaceAccent: styles.plosa,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Lang } from "@/constants/languages"
|
||||
import { membershipLevels } from "@/constants/membershipLevels"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import levelsData from "./data/EN.json"
|
||||
|
||||
@@ -7,7 +7,7 @@ import MobileTable from "./Mobile"
|
||||
|
||||
import type {
|
||||
ClientEarnAndBurnProps,
|
||||
TransactionsObject,
|
||||
TransactionsNonNullResponseObject,
|
||||
} from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export default function ClientEarnAndBurn({
|
||||
@@ -34,7 +34,7 @@ export default function ClientEarnAndBurn({
|
||||
// later on when we handle errors appropriately.
|
||||
const filteredTransactions = (data?.pages.filter(
|
||||
(page) => page && page.data
|
||||
) ?? []) as unknown as TransactionsObject[]
|
||||
) ?? []) as unknown as TransactionsNonNullResponseObject[]
|
||||
const transactions = filteredTransactions.flatMap((page) => page.data)
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -56,13 +56,11 @@ export default function ClientPreviousStays({
|
||||
stay={stay}
|
||||
/>
|
||||
))}
|
||||
</Grids.Stackable >
|
||||
{
|
||||
hasNextPage ? (
|
||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
||||
) : null
|
||||
}
|
||||
</ListContainer >
|
||||
</Grids.Stackable>
|
||||
{hasNextPage ? (
|
||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
||||
) : null}
|
||||
</ListContainer>
|
||||
) : (
|
||||
<EmptyPreviousStaysBlock />
|
||||
)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client"
|
||||
import { useEffect } from "react"
|
||||
import { useFormStatus } from "react-dom"
|
||||
|
||||
// import { useWatch } from "react-hook-form"
|
||||
// import { useIntl } from "react-intl"
|
||||
import { useProfileStore } from "@/stores/edit-profile"
|
||||
|
||||
import type { EditFormContentProps } from "@/types/components/myPages/myProfile/edit"
|
||||
|
||||
export default function FormContent({ control }: EditFormContentProps) {
|
||||
// const { formatMessage } = useIntl()
|
||||
const { pending } = useFormStatus()
|
||||
const setIsPending = useProfileStore((store) => store.setIsPending)
|
||||
// const country = useWatch({ name: "address.country" })
|
||||
|
||||
useEffect(() => {
|
||||
setIsPending(pending)
|
||||
}, [pending, setIsPending])
|
||||
|
||||
// const city = formatMessage({ id: "City" })
|
||||
// const email = formatMessage({ id: "Email" })
|
||||
// const street = formatMessage({ id: "Street" })
|
||||
// const zipCode = formatMessage({ id: "Zip code" })
|
||||
|
||||
return <></>
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.form {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect } from "react"
|
||||
import { useFormState as useReactFormState } from "react-dom"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
|
||||
import { useProfileStore } from "@/stores/edit-profile"
|
||||
|
||||
import { editProfile } from "@/actions/editProfile"
|
||||
|
||||
import FormContent from "./Content"
|
||||
import { type EditProfileSchema, editProfileSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
import {
|
||||
type EditFormProps,
|
||||
type State,
|
||||
} from "@/types/components/myPages/myProfile/edit"
|
||||
|
||||
export default function Form({ user }: EditFormProps) {
|
||||
const isValid = useProfileStore((store) => store.valid)
|
||||
const setValid = useProfileStore((store) => store.setValid)
|
||||
/**
|
||||
* like react, react-hook-form also exports a useFormState hook,
|
||||
* we want to clearly keep them separate by naming.
|
||||
*/
|
||||
const [state, formAction] = useReactFormState<State, FormData>(
|
||||
editProfile,
|
||||
null
|
||||
)
|
||||
|
||||
const form = useForm<EditProfileSchema>({
|
||||
defaultValues: user,
|
||||
criteriaMode: "all",
|
||||
mode: "onTouched",
|
||||
resolver: zodResolver(editProfileSchema),
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isValid !== form.formState.isValid) {
|
||||
setValid(form.formState.isValid)
|
||||
}
|
||||
}, [form.formState.isValid, isValid, setValid])
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form action={formAction} className={styles.form} id="edit-profile">
|
||||
<FormContent control={form.control} />
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
export const editProfileSchema = z.object({
|
||||
"address.country": z.string().min(1),
|
||||
"address.city": z.string().optional(),
|
||||
"address.streetAddress": z.string().optional(),
|
||||
"address.zipCode": z.string().min(1),
|
||||
dateOfBirth: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phoneNumber: phoneValidator(
|
||||
"Phone is required",
|
||||
"Please enter a valid phone number"
|
||||
),
|
||||
})
|
||||
|
||||
export type EditProfileSchema = z.infer<typeof editProfileSchema>
|
||||
@@ -1,11 +0,0 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import Form from "./Form"
|
||||
|
||||
export default async function EditProfile() {
|
||||
const user = await serverClient().user.get()
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return <Form user={user} />
|
||||
}
|
||||
6
components/Profile/Header/header.module.css
Normal file
6
components/Profile/Header/header.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-content: space-between;
|
||||
}
|
||||
5
components/Profile/Header/index.tsx
Normal file
5
components/Profile/Header/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import styles from "./header.module.css"
|
||||
|
||||
export default function Header({ children }: React.PropsWithChildren) {
|
||||
return <header className={styles.header}>{children}</header>
|
||||
}
|
||||
@@ -1,167 +1,411 @@
|
||||
.btn {
|
||||
background: none;
|
||||
border: none;
|
||||
/* No variable yet for radius 50px */
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
/* TODO: Waiting for variables for buttons from Design team */
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-weight: 600;
|
||||
line-height: 150%;
|
||||
letter-spacing: 1%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
transition:
|
||||
background-color 300ms ease,
|
||||
color 300ms ease;
|
||||
|
||||
/* TODO: Waiting for variables for buttons from Design team */
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.6%;
|
||||
}
|
||||
|
||||
/* INTENT */
|
||||
.primary,
|
||||
a.primary {
|
||||
background-color: var(--background-color);
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
.primary:hover,
|
||||
a.primary:hover,
|
||||
.primary:active,
|
||||
a.primary:active,
|
||||
.primary:focus,
|
||||
a.primary:focus {
|
||||
background-color: var(--hover-background);
|
||||
color: var(--hover-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.secondary,
|
||||
a.secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--background-color);
|
||||
color: var(--background-color);
|
||||
background: none;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.secondary:hover,
|
||||
a.secondary:hover,
|
||||
.secondary:active,
|
||||
a.secondary:active,
|
||||
.secondary:focus,
|
||||
a.secondary:focus {
|
||||
border: 1px solid var(--hover-color);
|
||||
color: var(--hover-color);
|
||||
.tertiary,
|
||||
a.tertiary {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inverted,
|
||||
a.inverted {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* VARIANTS */
|
||||
.default,
|
||||
a.default {
|
||||
align-items: center;
|
||||
border-radius: 50px;
|
||||
/* No variable yet */
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
align-items: baseline;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Disabled styles */
|
||||
/* SIZES */
|
||||
.small {
|
||||
gap: var(--Spacing-x-quarter);
|
||||
height: 40px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.medium {
|
||||
gap: var(--Spacing-x-quarter);
|
||||
height: 48px;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.large {
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 56px;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
/* DISABLED */
|
||||
.btn:disabled {
|
||||
background-color: var(--disabled-background-color);
|
||||
color: var(--disabled-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.small {
|
||||
font-size: 14px;
|
||||
gap: var(--Spacing-x2);
|
||||
height: 40px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
/* THEMES */
|
||||
.basePrimary {
|
||||
background-color: var(--Base-Button-Primary-Fill-Normal);
|
||||
color: var(--Base-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 16px;
|
||||
height: 30px;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
.basePrimary:active,
|
||||
.basePrimary:focus,
|
||||
.basePrimary:hover {
|
||||
background-color: var(--Base-Button-Primary-Fill-Hover);
|
||||
color: var(--Base-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 16px;
|
||||
height: 50px;
|
||||
gap: var(--Spacing-x-half);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
.basePrimary:disabled {
|
||||
background-color: var(--Base-Button-Primary-Fill-Disabled);
|
||||
color: var(--Base-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryLight {
|
||||
--font-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Normal);
|
||||
--background-color: var(--Theme-Primary-Light-Button-Primary-Fill-Normal);
|
||||
--hover-background: var(--Theme-Primary-Light-Button-Primary-Fill-Hover);
|
||||
--hover-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Hover);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Primary-Light-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Disabled);
|
||||
.baseSecondary {
|
||||
background-color: var(--Base-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Base-Button-Secondary-Border-Normal);
|
||||
color: var(--Base-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryDark {
|
||||
--font-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Normal);
|
||||
--background-color: var(--Theme-Primary-Dark-Button-Primary-Fill-Normal);
|
||||
--hover-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Hover);
|
||||
--hover-background: var(--Theme-Primary-Dark-Button-Primary-Fill-Hover);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Primary-Dark-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Disabled);
|
||||
.baseSecondary:active,
|
||||
.baseSecondary:focus,
|
||||
.baseSecondary:hover {
|
||||
background-color: var(--Base-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Base-Button-Secondary-Border-Hover);
|
||||
color: var(--Base-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryStrong {
|
||||
--background-color: var(--Theme-Primary-Strong-Button-Primary-Fill-Normal);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Primary-Strong-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(--Theme-Primary-Strong-Button-Primary-On-Fill-Disabled);
|
||||
--font-color: var(--Theme-Primary-Strong-Button-Primary-On-Fill-Normal);
|
||||
--hover-background: var(--Theme-Primary-Strong-Button-Primary-Fill-Hover);
|
||||
--hover-color: var(--Theme-Primary-Strong-Button-Primary-On-Fill-Hover);
|
||||
.baseSecondary:disabled {
|
||||
background-color: var(--Base-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Base-Button-Secondary-Border-Disabled);
|
||||
color: var(--Base-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.secondaryLight {
|
||||
--font-color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Normal);
|
||||
--background-color: var(--Theme-Secondary-Light-Button-Primary-Fill-Normal);
|
||||
--hover-color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Hover);
|
||||
--hover-background: var(--Theme-Secondary-Light-Button-Primary-Fill-Hover);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Secondary-Light-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(
|
||||
--Theme-Secondary-Light-Button-Primary-On-Fill-Disabled
|
||||
);
|
||||
.baseTertiary {
|
||||
background-color: var(--Base-Button-Tertiary-Fill-Normal);
|
||||
color: var(--Base-Button-Tertiary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.secondaryDark {
|
||||
--font-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Normal);
|
||||
--background-color: var(--Theme-Secondary-Dark-Button-Primary-Fill-Normal);
|
||||
--hover-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Hover);
|
||||
--hover-background: var(--Theme-Secondary-Dark-Button-Primary-Fill-Hover);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Secondary-Dark-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Disabled);
|
||||
.baseTertiary:active,
|
||||
.baseTertiary:focus,
|
||||
.baseTertiary:hover {
|
||||
background-color: var(--Base-Button-Tertiary-Fill-Hover);
|
||||
color: var(--Base-Button-Tertiary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.tertiaryLight {
|
||||
--font-color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Normal);
|
||||
--background-color: var(--Theme-Tertiary-Light-Button-Primary-Fill-Normal);
|
||||
--hover-color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Hover);
|
||||
--hover-background: var(--Theme-Tertiary-Light-Button-Primary-Fill-Hover);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Tertiary-Light-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Disabled);
|
||||
.baseTertiary:disabled {
|
||||
background-color: var(--Base-Button-Tertiary-Fill-Disabled);
|
||||
color: var(--Base-Button-Tertiary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.tertiaryDark {
|
||||
--font-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Normal);
|
||||
--background-color: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Normal);
|
||||
--hover-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Hover);
|
||||
--hover-background: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Hover);
|
||||
--disabled-background-color: var(
|
||||
--Theme-Tertiary-Dark-Button-Primary-Fill-Disabled
|
||||
);
|
||||
--disabled-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Disabled);
|
||||
.baseInverted {
|
||||
background-color: var(--Base-Button-Inverted-Fill-Normal);
|
||||
color: var(--Base-Button-Inverted-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.baseInverted:active,
|
||||
.baseInverted:focus,
|
||||
.baseInverted:hover {
|
||||
background-color: var(--Base-Button-Inverted-Fill-Hover);
|
||||
color: var(--Base-Button-Inverted-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.baseInverted:disabled {
|
||||
background-color: var(--Base-Button-Inverted-Fill-Disabled);
|
||||
color: var(--Base-Button-Inverted-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryStrongPrimary {
|
||||
background-color: var(--Theme-Primary-Strong-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Primary-Strong-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryStrongPrimary:active,
|
||||
.primaryStrongPrimary:focus,
|
||||
.primaryStrongPrimary:hover {
|
||||
background-color: var(--Theme-Primary-Strong-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Primary-Strong-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryStrongPrimary:disabled {
|
||||
background-color: var(--Theme-Primary-Strong-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Primary-Strong-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryStrongSecondary {
|
||||
background-color: var(--Theme-Primary-Strong-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Theme-Primary-Strong-Button-Secondary-Border-Normal);
|
||||
color: var(--Theme-Primary-Strong-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryStrongSecondary:active,
|
||||
.primaryStrongSecondary:focus,
|
||||
.primaryStrongSecondary:hover {
|
||||
background-color: var(--Theme-Primary-Strong-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Theme-Primary-Strong-Button-Secondary-Border-Hover);
|
||||
color: var(--Theme-Primary-Strong-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryStrongSecondary:disabled {
|
||||
background-color: var(--Theme-Primary-Strong-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Theme-Primary-Strong-Button-Secondary-Border-Disabled);
|
||||
color: var(--Theme-Primary-Strong-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryDarkPrimary {
|
||||
background-color: var(--Theme-Primary-Dark-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryDarkPrimary:active,
|
||||
.primaryDarkPrimary:focus,
|
||||
.primaryDarkPrimary:hover {
|
||||
background-color: var(--Theme-Primary-Dark-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryDarkPrimary:disabled {
|
||||
background-color: var(--Theme-Primary-Dark-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryDarkSecondary {
|
||||
background-color: var(--Theme-Primary-Dark-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Theme-Primary-Dark-Button-Secondary-Border-Normal);
|
||||
color: var(--Theme-Primary-Dark-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryDarkSecondary:active,
|
||||
.primaryDarkSecondary:focus,
|
||||
.primaryDarkSecondary:hover {
|
||||
background-color: var(--Theme-Primary-Dark-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Theme-Primary-Dark-Button-Secondary-Border-Hover);
|
||||
color: var(--Theme-Primary-Dark-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryDarkSecondary:disabled {
|
||||
background-color: var(--Theme-Primary-Dark-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Theme-Primary-Dark-Button-Secondary-Border-Disabled);
|
||||
color: var(--Theme-Primary-Dark-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryLightPrimary {
|
||||
background-color: var(--Theme-Primary-Light-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryLightPrimary:active,
|
||||
.primaryLightPrimary:focus,
|
||||
.primaryLightPrimary:hover {
|
||||
background-color: var(--Theme-Primary-Light-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryLightPrimary:disabled {
|
||||
background-color: var(--Theme-Primary-Light-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.primaryLightSecondary {
|
||||
background-color: var(--Theme-Primary-Light-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Theme-Primary-Light-Button-Secondary-Border-Normal);
|
||||
color: var(--Theme-Primary-Light-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.primaryLightSecondary:active,
|
||||
.primaryLightSecondary:focus,
|
||||
.primaryLightSecondary:hover {
|
||||
background-color: var(--Theme-Primary-Light-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Theme-Primary-Light-Button-Secondary-Border-Hover);
|
||||
color: var(--Theme-Primary-Light-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.primaryLightSecondary:disabled {
|
||||
background-color: var(--Theme-Primary-Light-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Theme-Primary-Light-Button-Secondary-Border-Disabled);
|
||||
color: var(--Theme-Primary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.secondaryDarkPrimary {
|
||||
background-color: var(--Theme-Secondary-Dark-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.secondaryDarkPrimary:active,
|
||||
.secondaryDarkPrimary:focus,
|
||||
.secondaryDarkPrimary:hover {
|
||||
background-color: var(--Theme-Secondary-Dark-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.secondaryDarkPrimary:disabled {
|
||||
background-color: var(--Theme-Secondary-Dark-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.secondaryDarkSecondary {
|
||||
background-color: var(--Theme-Secondary-Dark-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Theme-Secondary-Dark-Button-Secondary-Border-Normal);
|
||||
color: var(--Theme-Secondary-Dark-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.secondaryDarkSecondary:active,
|
||||
.secondaryDarkSecondary:focus,
|
||||
.secondaryDarkSecondary:hover {
|
||||
background-color: var(--Theme-Secondary-Dark-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Theme-Secondary-Dark-Button-Secondary-Border-Hover);
|
||||
color: var(--Theme-Secondary-Dark-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.secondaryDarkSecondary:disabled {
|
||||
background-color: var(--Theme-Secondary-Dark-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Theme-Secondary-Dark-Button-Secondary-Border-Disabled);
|
||||
color: var(--Theme-Secondary-Dark-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.secondaryLightPrimary {
|
||||
background-color: var(--Theme-Secondary-Light-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.secondaryLightPrimary:active,
|
||||
.secondaryLightPrimary:focus,
|
||||
.secondaryLightPrimary:hover {
|
||||
background-color: var(--Theme-Secondary-Light-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.secondaryLightPrimary:disabled {
|
||||
background-color: var(--Theme-Secondary-Light-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.secondaryLightSecondary {
|
||||
background-color: var(--Theme-Secondary-Light-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Theme-Secondary-Light-Button-Secondary-Border-Normal);
|
||||
color: var(--Theme-Secondary-Light-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.secondaryLightSecondary:active,
|
||||
.secondaryLightSecondary:focus,
|
||||
.secondaryLightSecondary:hover {
|
||||
background-color: var(--Theme-Secondary-Light-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Theme-Secondary-Light-Button-Secondary-Border-Hover);
|
||||
color: var(--Theme-Secondary-Light-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.secondaryLightSecondary:disabled {
|
||||
background-color: var(--Theme-Secondary-Light-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Theme-Secondary-Light-Button-Secondary-Border-Disabled);
|
||||
color: var(--Theme-Secondary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.tertiaryDarkPrimary {
|
||||
background-color: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.tertiaryDarkPrimary:active,
|
||||
.tertiaryDarkPrimary:focus,
|
||||
.tertiaryDarkPrimary:hover {
|
||||
background-color: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.tertiaryDarkPrimary:disabled {
|
||||
background-color: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.tertiaryDarkSecondary {
|
||||
background-color: var(--Theme-Tertiary-Dark-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Theme-Tertiary-Dark-Button-Secondary-Border-Normal);
|
||||
color: var(--Theme-Tertiary-Dark-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.tertiaryDarkSecondary:active,
|
||||
.tertiaryDarkSecondary:focus,
|
||||
.tertiaryDarkSecondary:hover {
|
||||
background-color: var(--Theme-Tertiary-Dark-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Theme-Tertiary-Dark-Button-Secondary-Border-Hover);
|
||||
color: var(--Theme-Tertiary-Dark-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.tertiaryDarkSecondary:disabled {
|
||||
background-color: var(--Theme-Tertiary-Dark-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Theme-Tertiary-Dark-Button-Secondary-Border-Disabled);
|
||||
color: var(--Theme-Tertiary-Dark-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.tertiaryLightPrimary {
|
||||
background-color: var(--Theme-Tertiary-Light-Button-Primary-Fill-Normal);
|
||||
color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.tertiaryLightPrimary:active,
|
||||
.tertiaryLightPrimary:focus,
|
||||
.tertiaryLightPrimary:hover {
|
||||
background-color: var(--Theme-Tertiary-Light-Button-Primary-Fill-Hover);
|
||||
color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.tertiaryLightPrimary:disabled {
|
||||
background-color: var(--Theme-Tertiary-Light-Button-Primary-Fill-Disabled);
|
||||
color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
.tertiaryLightSecondary {
|
||||
background-color: var(--Tertiary-Light-Button-Secondary-Fill-Normal);
|
||||
border-color: var(--Tertiary-Light-Button-Secondary-Border-Normal);
|
||||
color: var(--Tertiary-Light-Button-Secondary-On-Fill-Normal);
|
||||
}
|
||||
|
||||
.tertiaryLightSecondary:active,
|
||||
.tertiaryLightSecondary:focus,
|
||||
.tertiaryLightSecondary:hover {
|
||||
background-color: var(--Tertiary-Light-Button-Secondary-Fill-Hover);
|
||||
border-color: var(--Tertiary-Light-Button-Secondary-Border-Hover);
|
||||
color: var(--Tertiary-Light-Button-Secondary-On-Fill-Hover);
|
||||
}
|
||||
|
||||
.tertiaryLightSecondary:disabled {
|
||||
background-color: var(--Tertiary-Light-Button-Secondary-Fill-Disabled);
|
||||
border-color: var(--Tertiary-Light-Button-Secondary-Border-Disabled);
|
||||
color: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import styles from "./button.module.css"
|
||||
export const buttonVariants = cva(styles.btn, {
|
||||
variants: {
|
||||
intent: {
|
||||
inverted: styles.inverted,
|
||||
primary: styles.primary,
|
||||
secondary: styles.secondary,
|
||||
tertiary: styles.tertiary,
|
||||
},
|
||||
size: {
|
||||
small: styles.small,
|
||||
@@ -14,13 +16,14 @@ export const buttonVariants = cva(styles.btn, {
|
||||
large: styles.large,
|
||||
},
|
||||
theme: {
|
||||
primaryDark: styles.primaryDark,
|
||||
primaryLight: styles.primaryLight,
|
||||
primaryStrong: styles.primaryStrong,
|
||||
secondaryLight: styles.secondaryLight,
|
||||
secondaryDark: styles.secondaryDark,
|
||||
tertiaryLight: styles.tertiaryLight,
|
||||
tertiaryDark: styles.tertiaryDark,
|
||||
base: "",
|
||||
primaryDark: "",
|
||||
primaryStrong: "",
|
||||
primaryLight: "",
|
||||
secondaryLight: "",
|
||||
secondaryDark: "",
|
||||
tertiaryLight: "",
|
||||
tertiaryDark: "",
|
||||
},
|
||||
variant: {
|
||||
default: styles.default,
|
||||
@@ -33,4 +36,97 @@ export const buttonVariants = cva(styles.btn, {
|
||||
theme: "primaryLight",
|
||||
variant: "default",
|
||||
},
|
||||
|
||||
compoundVariants: [
|
||||
{
|
||||
className: styles.basePrimary,
|
||||
intent: "primary",
|
||||
theme: "base",
|
||||
},
|
||||
{
|
||||
className: styles.baseSecondary,
|
||||
intent: "secondary",
|
||||
theme: "base",
|
||||
},
|
||||
{
|
||||
className: styles.baseTertiary,
|
||||
intent: "tertiary",
|
||||
theme: "base",
|
||||
},
|
||||
{
|
||||
className: styles.baseInverted,
|
||||
intent: "inverted",
|
||||
theme: "base",
|
||||
},
|
||||
{
|
||||
className: styles.primaryDarkPrimary,
|
||||
intent: "primary",
|
||||
theme: "primaryDark",
|
||||
},
|
||||
{
|
||||
className: styles.primaryDarkSecondary,
|
||||
intent: "secondary",
|
||||
theme: "primaryDark",
|
||||
},
|
||||
{
|
||||
className: styles.primaryLightPrimary,
|
||||
intent: "primary",
|
||||
theme: "primaryLight",
|
||||
},
|
||||
{
|
||||
className: styles.primaryLightSecondary,
|
||||
intent: "secondary",
|
||||
theme: "primaryLight",
|
||||
},
|
||||
{
|
||||
className: styles.primaryStrongPrimary,
|
||||
intent: "primary",
|
||||
theme: "primaryStrong",
|
||||
},
|
||||
{
|
||||
className: styles.primaryStrongSecondary,
|
||||
intent: "secondary",
|
||||
theme: "primaryStrong",
|
||||
},
|
||||
{
|
||||
className: styles.secondaryDarkPrimary,
|
||||
intent: "primary",
|
||||
theme: "secondaryDark",
|
||||
},
|
||||
{
|
||||
className: styles.secondaryDarkSecondary,
|
||||
intent: "secondary",
|
||||
theme: "secondaryDark",
|
||||
},
|
||||
{
|
||||
className: styles.secondaryLightPrimary,
|
||||
intent: "primary",
|
||||
theme: "secondaryLight",
|
||||
},
|
||||
{
|
||||
className: styles.secondaryLightSecondary,
|
||||
intent: "secondary",
|
||||
theme: "secondaryLight",
|
||||
},
|
||||
{
|
||||
className: styles.tertiaryDarkPrimary,
|
||||
intent: "primary",
|
||||
theme: "tertiaryDark",
|
||||
},
|
||||
{
|
||||
className: styles.tertiaryDarkSecondary,
|
||||
intent: "secondary",
|
||||
theme: "tertiaryDark",
|
||||
},
|
||||
{
|
||||
className: styles.tertiaryLightPrimary,
|
||||
intent: "primary",
|
||||
theme: "tertiaryLight",
|
||||
},
|
||||
{
|
||||
className: styles.tertiaryLightSecondary,
|
||||
intent: "secondary",
|
||||
theme: "tertiaryLight",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
.gridContainer {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.carousel {
|
||||
display: grid;
|
||||
grid-auto-columns: 80dvw;
|
||||
grid-auto-flow: column;
|
||||
gap: var(--Spacing-x2);
|
||||
margin-left: calc(0 - var(--Spacing-x2));
|
||||
margin-right: calc(0 - var(--Spacing-x2));
|
||||
padding-left: var(--Spacing-x2);
|
||||
overflow-x: scroll;
|
||||
scroll-padding-left: var(--Spacing-x2);
|
||||
scroll-snap-type: x mandatory;
|
||||
scrollbar-width: none;
|
||||
/* Hide scrollbar IE and Edge */
|
||||
-ms-overflow-style: none;
|
||||
/* Hide Scrollbar Firefox */
|
||||
}
|
||||
|
||||
.carousel:last-child {
|
||||
margin-right: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.carousel > * {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
/* Hide Scrollbar Chrome, Safari and Opera */
|
||||
.gridContainer::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.twoColumnGrid,
|
||||
.twoPlusOne {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.threeColumnGrid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.twoPlusOne > *:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
grid-auto-flow: unset;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,46 @@
|
||||
.container {
|
||||
--select-border: 2px solid var(--UI-Grey-60);
|
||||
--select-width: min(28rem, 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comboBoxContainer {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-color: var(--Scandic-Beige-40);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: grid;
|
||||
grid-template-areas: "content";
|
||||
width: var(--select-width);
|
||||
gap: var(--Spacing-x-half);
|
||||
grid-template-areas:
|
||||
"label chevron"
|
||||
"input chevron";
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
height: 60px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.label {
|
||||
grid-area: label;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border: var(--select-border);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
grid-area: content;
|
||||
height: 40px;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
width: var(--select-width);
|
||||
border: none;
|
||||
grid-area: input;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.input,
|
||||
.listBoxItem {
|
||||
color: var(--UI-Grey-60);
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
grid-area: content;
|
||||
grid-area: chevron;
|
||||
height: 100%;
|
||||
justify-self: flex-end;
|
||||
padding-left: 0;
|
||||
@@ -38,13 +49,27 @@
|
||||
|
||||
.popover {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border: var(--select-border);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
border-color: var(--Scandic-Beige-40);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
left: 0px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x1);
|
||||
width: var(--select-width);
|
||||
padding: var(--Spacing-x2);
|
||||
top: calc(60px + var(--Spacing-x1));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.listBoxItem {
|
||||
padding: 0 var(--Spacing-x1);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x1) var(--Spacing-x1)
|
||||
var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.listBoxItem[data-selected="true"],
|
||||
.listBoxItem:hover {
|
||||
background-color: var(--Scandic-Blue-00);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export type CountryProps = {
|
||||
label: string
|
||||
name?: string
|
||||
placeholder?: string
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
export type CountryPortalContainer = HTMLDivElement | undefined
|
||||
export type CountryPortalContainerArgs = HTMLDivElement | null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
import { ErrorMessage } from "@hookform/error-message"
|
||||
import { useRef } from "react"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
ComboBox,
|
||||
@@ -14,21 +14,33 @@ import {
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import SelectChevron from "@/components/TempDesignSystem/Form/SelectChevron"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import SelectChevron from "../SelectChevron"
|
||||
import { countries } from "./countries"
|
||||
|
||||
import styles from "./country.module.css"
|
||||
|
||||
import type { CountryProps } from "./country"
|
||||
import type {
|
||||
CountryPortalContainer,
|
||||
CountryPortalContainerArgs,
|
||||
CountryProps,
|
||||
} from "./country"
|
||||
|
||||
export default function CountrySelect({
|
||||
label,
|
||||
name = "country",
|
||||
registerOptions,
|
||||
registerOptions = {},
|
||||
}: CountryProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const divRef = useRef<HTMLDivElement>(null)
|
||||
const [rootDiv, setRootDiv] = useState<CountryPortalContainer>(undefined)
|
||||
|
||||
function setRef(node: CountryPortalContainerArgs) {
|
||||
if (node) {
|
||||
setRootDiv(node)
|
||||
}
|
||||
}
|
||||
const { control, setValue } = useFormContext()
|
||||
const { field } = useController({
|
||||
control,
|
||||
@@ -43,7 +55,7 @@ export default function CountrySelect({
|
||||
const selectCountryLabel = formatMessage({ id: "Select a country" })
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={divRef}>
|
||||
<div className={styles.container} ref={setRef}>
|
||||
<ComboBox
|
||||
aria-label={formatMessage({ id: "Select country of residence" })}
|
||||
className={styles.select}
|
||||
@@ -55,6 +67,13 @@ export default function CountrySelect({
|
||||
selectedKey={field.value}
|
||||
>
|
||||
<div className={styles.comboBoxContainer}>
|
||||
<Label
|
||||
className={styles.label}
|
||||
size="small"
|
||||
required={!!registerOptions.required}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<Body asChild fontOnly>
|
||||
<Input
|
||||
aria-label={selectCountryLabel}
|
||||
@@ -73,13 +92,14 @@ export default function CountrySelect({
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
shouldUpdatePosition={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={divRef.current ?? undefined}
|
||||
UNSTABLE_portalContainer={rootDiv}
|
||||
>
|
||||
<ListBox>
|
||||
{countries.map((country, idx) => (
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
/* Leaving, will most likely get deleted */
|
||||
.container {
|
||||
--border: 2px solid var(--some-black-color, #757575);
|
||||
--radius: 0.4rem;
|
||||
--width: min(28rem, 100%);
|
||||
|
||||
--width-day: 6rem;
|
||||
--width-month: 6rem;
|
||||
--width-year: 8rem;
|
||||
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
grid-template-areas: "day month year";
|
||||
grid-template-columns: min(--width-day, 1fr) min(--width-month, 1fr) min(
|
||||
--width-year,
|
||||
2fr
|
||||
);
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-areas: "year month day";
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
width: var(--width);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { Control, RegisterOptions } from "react-hook-form"
|
||||
|
||||
import type { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export const enum DateName {
|
||||
date = "date",
|
||||
@@ -9,7 +7,6 @@ export const enum DateName {
|
||||
}
|
||||
export interface DateProps
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
control: Control<EditProfileSchema>
|
||||
name: keyof EditProfileSchema
|
||||
registerOptions: RegisterOptions<EditProfileSchema>
|
||||
name: string
|
||||
registerOptions: RegisterOptions
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
import { rangeArray } from "@/utils/rangeArray"
|
||||
|
||||
import Select from "../Select"
|
||||
import { DateName } from "./date"
|
||||
|
||||
import styles from "./date.module.css"
|
||||
@@ -23,14 +23,10 @@ import type { Key } from "react-aria-components"
|
||||
import type { DateProps } from "./date"
|
||||
|
||||
/** TODO: Get selecting with Enter-key to work */
|
||||
export default function DateSelect({
|
||||
control,
|
||||
name,
|
||||
registerOptions,
|
||||
}: DateProps) {
|
||||
export default function DateSelect({ name, registerOptions }: DateProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const d = useWatch({ name })
|
||||
const { setValue } = useFormContext()
|
||||
const { control, setValue } = useFormContext()
|
||||
const { field } = useController({
|
||||
control,
|
||||
name,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
.message {
|
||||
align-items: center;
|
||||
color: var(--Scandic-Red-60);
|
||||
margin: var(--Spacing-x-half) 0 0;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
margin: var(--Spacing-x1) 0 0;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
|
||||
@@ -15,9 +16,10 @@ export default function ErrorMessage<T>({
|
||||
errors={errors}
|
||||
name={name}
|
||||
render={({ message }) => (
|
||||
<Body className={styles.message} fontOnly>
|
||||
<Caption className={styles.message} fontOnly>
|
||||
<InfoCircleIcon color="red" />
|
||||
{message}
|
||||
</Body>
|
||||
</Caption>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client"
|
||||
import { Input as AriaInput, TextField } from "react-aria-components"
|
||||
import { useController } from "react-hook-form"
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./input.module.css"
|
||||
@@ -11,17 +16,24 @@ import type { InputProps } from "./input"
|
||||
|
||||
export default function Input({
|
||||
"aria-label": ariaLabel,
|
||||
control,
|
||||
disabled,
|
||||
label,
|
||||
name,
|
||||
placeholder,
|
||||
registerOptions,
|
||||
placeholder = "",
|
||||
registerOptions = {},
|
||||
required = false,
|
||||
type = "text",
|
||||
}: InputProps) {
|
||||
const { control } = useFormContext()
|
||||
const rules = {
|
||||
...registerOptions,
|
||||
required:
|
||||
"required" in registerOptions ? !!registerOptions.required : required,
|
||||
}
|
||||
const { field, fieldState, formState } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
rules,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -32,18 +44,24 @@ export default function Input({
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions?.required}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={field.onChange}
|
||||
type={type}
|
||||
>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</Body>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,44 @@
|
||||
.input {
|
||||
border: 2px solid var(--UI-Grey-60);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
color: var(--UI-Grey-60);
|
||||
height: 40px;
|
||||
.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);
|
||||
width: min(280px, 100%);
|
||||
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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Control, RegisterOptions } from "react-hook-form"
|
||||
|
||||
import { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
control: Control<EditProfileSchema>
|
||||
name: keyof EditProfileSchema
|
||||
registerOptions?: RegisterOptions<EditProfileSchema>
|
||||
label: string
|
||||
name: string
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
20
components/TempDesignSystem/Form/Label/index.tsx
Normal file
20
components/TempDesignSystem/Form/Label/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { labelVariants } from "./variants"
|
||||
|
||||
import type { LabelProps } from "./label"
|
||||
|
||||
export default function Label({
|
||||
children,
|
||||
className,
|
||||
required,
|
||||
size,
|
||||
}: LabelProps) {
|
||||
const classNames = labelVariants({
|
||||
className,
|
||||
size,
|
||||
})
|
||||
return (
|
||||
<span className={classNames}>
|
||||
{children} {required ? "*" : ""}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
30
components/TempDesignSystem/Form/Label/label.module.css
Normal file
30
components/TempDesignSystem/Form/Label/label.module.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.label {
|
||||
color: var(--UI-Grey-60);
|
||||
font-family: "fira sans";
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.03px;
|
||||
line-height: 120%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
span.small {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span.regular {
|
||||
font-size: 16px;
|
||||
order: 1;
|
||||
transition: font-size 200ms ease 100ms;
|
||||
}
|
||||
|
||||
input:focus ~ .label,
|
||||
input:not(:placeholder-shown) ~ .label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input:placeholder-shown ~ .label {
|
||||
align-self: center;
|
||||
grid-row: 1/-1;
|
||||
}
|
||||
9
components/TempDesignSystem/Form/Label/label.ts
Normal file
9
components/TempDesignSystem/Form/Label/label.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { labelVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
export interface LabelProps
|
||||
extends React.PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>,
|
||||
VariantProps<typeof labelVariants> {
|
||||
required?: boolean
|
||||
}
|
||||
15
components/TempDesignSystem/Form/Label/variants.ts
Normal file
15
components/TempDesignSystem/Form/Label/variants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./label.module.css"
|
||||
|
||||
export const labelVariants = cva(styles.label, {
|
||||
variants: {
|
||||
size: {
|
||||
small: styles.small,
|
||||
regular: styles.regular,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "regular",
|
||||
},
|
||||
})
|
||||
@@ -1,99 +1,137 @@
|
||||
"use client"
|
||||
import "react-international-phone/style.css"
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import { parsePhoneNumber } from "libphonenumber-js"
|
||||
import {
|
||||
Input as AriaInput,
|
||||
Label as AriaLabel,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||
import {
|
||||
defaultCountries,
|
||||
getCountry,
|
||||
PhoneInput,
|
||||
type PhoneInputRefType,
|
||||
CountrySelector,
|
||||
DialCodePreview,
|
||||
ParsedCountry,
|
||||
usePhoneInput,
|
||||
} from "react-international-phone"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./phone.module.css"
|
||||
|
||||
import type { ChangeEvent } from "react"
|
||||
|
||||
import type { PhoneProps } from "./phone"
|
||||
|
||||
export default function Phone({
|
||||
countrySelectName = "country",
|
||||
ariaLabel = "Phone number input",
|
||||
disabled = false,
|
||||
label,
|
||||
name = "phoneNumber",
|
||||
placeholder = "",
|
||||
registerOptions = {
|
||||
required: true,
|
||||
},
|
||||
}: PhoneProps) {
|
||||
const phoneRef = useRef<PhoneInputRefType>(null)
|
||||
const { control, formState } = useFormContext()
|
||||
const countryValue = useWatch({ name: countrySelectName })
|
||||
const defaultCountry = getCountry({
|
||||
countries: defaultCountries,
|
||||
field: "iso2",
|
||||
value: String(countryValue).toLowerCase(),
|
||||
})
|
||||
/**
|
||||
* Holds the previous selected country to be able to update
|
||||
* countrycode based on country select field.
|
||||
* Since PhoneInput inputs the countrys dialcode (country code) upon
|
||||
* selection, we need to check if the current value is just
|
||||
* the previously selected countrys dialcode number.
|
||||
*/
|
||||
const prevSelectedCountry = useRef<string | undefined>(countryValue)
|
||||
const { field } = useController({
|
||||
const { formatMessage } = useIntl()
|
||||
const { control, setValue } = useFormContext()
|
||||
const phone = useWatch({ name })
|
||||
|
||||
const { field, fieldState, formState } = useController({
|
||||
control,
|
||||
disabled,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
|
||||
const handleCountrySelectForPhone = useCallback((country: string) => {
|
||||
const selectedCountry = getCountry({
|
||||
countries: defaultCountries,
|
||||
field: "iso2",
|
||||
value: country.toLowerCase(),
|
||||
const { country, handlePhoneValueChange, inputValue, setCountry } =
|
||||
usePhoneInput({
|
||||
defaultCountry:
|
||||
parsePhoneNumber(
|
||||
formState.defaultValues?.phoneNumber
|
||||
).country?.toLowerCase() ?? "sv",
|
||||
disableCountryGuess: true,
|
||||
forceDialCode: true,
|
||||
value: phone,
|
||||
})
|
||||
|
||||
if (selectedCountry) {
|
||||
phoneRef.current?.setCountry(selectedCountry.iso2)
|
||||
prevSelectedCountry.current = country.toLowerCase()
|
||||
}
|
||||
}, [])
|
||||
function handleSelectCountry(value: ParsedCountry) {
|
||||
setCountry(value.iso2)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (countryValue) {
|
||||
if (field.value) {
|
||||
if (prevSelectedCountry.current) {
|
||||
if (prevSelectedCountry.current !== countryValue) {
|
||||
const selectedCountryPrev = getCountry({
|
||||
countries: defaultCountries,
|
||||
field: "iso2",
|
||||
value: prevSelectedCountry.current.toLowerCase(),
|
||||
})
|
||||
if (
|
||||
field.value.replace("+", "") === selectedCountryPrev?.dialCode
|
||||
) {
|
||||
handleCountrySelectForPhone(countryValue)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleCountrySelectForPhone(countryValue)
|
||||
}
|
||||
} else {
|
||||
handleCountrySelectForPhone(countryValue)
|
||||
}
|
||||
}
|
||||
}, [countryValue, field.value, handleCountrySelectForPhone])
|
||||
function handleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||
handlePhoneValueChange(evt)
|
||||
setValue(name, evt.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.phone}>
|
||||
<PhoneInput
|
||||
{...field}
|
||||
className={styles.input}
|
||||
defaultCountry={defaultCountry?.iso2 ?? "se"}
|
||||
placeholder={placeholder}
|
||||
<CountrySelector
|
||||
dropdownArrowClassName={styles.arrow}
|
||||
flagClassName={styles.flag}
|
||||
onSelect={handleSelectCountry}
|
||||
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
|
||||
ref={phoneRef}
|
||||
selectedCountry={country.iso2}
|
||||
renderButtonWrapper={(props) => (
|
||||
<button
|
||||
{...props.rootProps}
|
||||
className={styles.select}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<Label required={!!registerOptions.required} size="small">
|
||||
{formatMessage({ id: "Country code" })}
|
||||
</Label>
|
||||
<div className={styles.selectContainer}>
|
||||
{props.children}
|
||||
<Body asChild fontOnly>
|
||||
<DialCodePreview
|
||||
className={styles.dialCode}
|
||||
dialCode={country.dialCode}
|
||||
prefix="+"
|
||||
/>
|
||||
</Body>
|
||||
<ChevronDownIcon
|
||||
className={styles.chevron}
|
||||
color="grey80"
|
||||
height={18}
|
||||
width={18}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
aria-label={ariaLabel}
|
||||
defaultValue={field.value}
|
||||
isDisabled={disabled ?? field.disabled}
|
||||
isInvalid={fieldState.invalid}
|
||||
isRequired={!!registerOptions?.required}
|
||||
name={field.name}
|
||||
type="tel"
|
||||
>
|
||||
<AriaLabel className={styles.inputContainer} htmlFor={field.name}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
required={!!registerOptions.required}
|
||||
value={inputValue}
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!registerOptions.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
<ErrorMessage errors={formState.errors} name={field.name} />
|
||||
</TextField>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,26 +1,130 @@
|
||||
.phone {
|
||||
--react-international-phone-border-color: var(--UI-Grey-60);
|
||||
--react-international-phone-border-radius: var(--Corner-radius-Small);
|
||||
--react-international-phone-font-size: var(
|
||||
--typography-Body-Regular-fontSize
|
||||
);
|
||||
--react-international-phone-height: 40px;
|
||||
--react-international-phone-text-color: var(--UI-Grey-60);
|
||||
}
|
||||
|
||||
.phone :global(.react-international-phone-input-container) {
|
||||
display: grid;
|
||||
/* r-i-p sets their width dynamically and doesn't respect the width property of its parent */
|
||||
grid-template-columns: 470px minmax(203px, 1fr);
|
||||
width: min(280px, 100%);
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: max(164px) 1fr;
|
||||
|
||||
--react-international-phone-background-color: var(--Main-Grey-White);
|
||||
--react-international-phone-border-color: var(--Scandic-Beige-40);
|
||||
--react-international-phone-dropdown-preferred-list-divider-color: var(
|
||||
--Scandic-Brand-Pale-Peach
|
||||
);
|
||||
--react-international-phone-selected-dropdown-item-background-color: var(
|
||||
--Scandic-Blue-00
|
||||
);
|
||||
--react-international-phone-text-color: var(--Main-Grey-100);
|
||||
|
||||
--react-international-phone-dropdown-preferred-list-divider-margin: 8px;
|
||||
|
||||
--react-international-phone-height: 60px;
|
||||
--react-international-phone-dropdown-top: calc(
|
||||
var(--react-international-phone-height) + var(--Spacing-x1)
|
||||
);
|
||||
}
|
||||
|
||||
/* react-international-phone only exposes variables to change border-color */
|
||||
.phone :global(.react-international-phone-country-selector-button),
|
||||
.phone :global(.react-international-phone-input) {
|
||||
border-width: 2px;
|
||||
.phone:has(.input:active, .input:focus) {
|
||||
--react-international-phone-border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.phone :global(.react-international-phone-input) {
|
||||
.phone :global(.react-international-phone-country-selector-dropdown) {
|
||||
background: var(--Main-Grey-White);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.08);
|
||||
gap: var(--Spacing-x1);
|
||||
outline: none;
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.phone
|
||||
:global(.react-international-phone-country-selector-dropdown__list-item) {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x1) var(--Spacing-x1)
|
||||
var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.phone
|
||||
:global(.react-international-phone-country-selector-button__button-content) {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.inputContainer,
|
||||
.select {
|
||||
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;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select[aria-expanded="true"] .chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.selectContainer {
|
||||
background-color: var(--Main-Grey-White);
|
||||
border: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
grid-template-columns: auto 1fr auto;
|
||||
height: 18px;
|
||||
justify-content: flex-start;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flag {
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.select .dialCode {
|
||||
border: none;
|
||||
color: var(--Main-Grey-100);
|
||||
line-height: 1;
|
||||
justify-self: flex-start;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inputContainer:has(.input:not(:focus):placeholder-shown) {
|
||||
gap: 0;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.inputContainer:has(.input:active, .input:focus) {
|
||||
border-color: var(--Scandic-Blue-90);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export type PhoneProps = {
|
||||
countrySelectName?: string
|
||||
ariaLabel?: string
|
||||
disabled?: boolean
|
||||
label: string
|
||||
name?: string
|
||||
placeholder?: string
|
||||
registerOptions?: RegisterOptions
|
||||
|
||||
@@ -1,94 +1,35 @@
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
type Key,
|
||||
Label,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
Select as ReactAriaSelect,
|
||||
SelectValue,
|
||||
} from "react-aria-components"
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
|
||||
import Body from "../../Text/Body"
|
||||
import Footnote from "../../Text/Footnote"
|
||||
import SelectChevron from "../SelectChevron"
|
||||
import ReactAriaSelect from "@/components/TempDesignSystem/Select"
|
||||
|
||||
import styles from "./select.module.css"
|
||||
|
||||
import type { SelectPortalContainer, SelectProps } from "./select"
|
||||
import type { SelectProps } from "./select"
|
||||
|
||||
export default function Select({
|
||||
"aria-label": ariaLabel,
|
||||
items,
|
||||
label,
|
||||
onSelect,
|
||||
name,
|
||||
placeholder,
|
||||
value,
|
||||
defaultSelectedKey,
|
||||
registerOptions = {},
|
||||
}: SelectProps) {
|
||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(null)
|
||||
|
||||
function setRef(node: SelectPortalContainer) {
|
||||
if (node) {
|
||||
setRootDiv(node)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnSelect(key: Key) {
|
||||
onSelect(key)
|
||||
}
|
||||
const { control } = useFormContext()
|
||||
const { field } = useController({
|
||||
control,
|
||||
name,
|
||||
rules: registerOptions,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={setRef}>
|
||||
<ReactAriaSelect
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
aria-label={ariaLabel}
|
||||
className={styles.select}
|
||||
onSelectionChange={handleOnSelect}
|
||||
placeholder={placeholder}
|
||||
selectedKey={value as Key}
|
||||
>
|
||||
<Body asChild fontOnly>
|
||||
<Button className={styles.input}>
|
||||
<div className={styles.inputContentWrapper}>
|
||||
<Footnote asChild fontOnly>
|
||||
<Label className={styles.label}>{label}</Label>
|
||||
</Footnote>
|
||||
<SelectValue />
|
||||
</div>
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
</Body>
|
||||
<Body asChild fontOnly>
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={rootDiv ?? undefined}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<ListBoxItem
|
||||
aria-label={String(item)}
|
||||
className={styles.listBoxItem}
|
||||
id={item.value}
|
||||
key={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Body>
|
||||
</ReactAriaSelect>
|
||||
</div>
|
||||
<ReactAriaSelect
|
||||
defaultSelectedKey={field.value}
|
||||
disabled={field.disabled}
|
||||
items={items}
|
||||
label={label}
|
||||
name={field.name}
|
||||
onBlur={field.onBlur}
|
||||
onSelect={field.onChange}
|
||||
placeholder={placeholder}
|
||||
value={field.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { Key } from "react-aria-components"
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
import type { SelectProps as ReactAriaSelectProps } from "@/components/TempDesignSystem/Select/select"
|
||||
|
||||
export interface SelectProps
|
||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {
|
||||
items: { label: string; value: Key }[]
|
||||
label: string
|
||||
name: string
|
||||
onSelect: (key: Key) => void
|
||||
placeholder?: string
|
||||
defaultSelectedKey?: Key
|
||||
extends Omit<
|
||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||
"name" | "onSelect"
|
||||
>,
|
||||
Omit<ReactAriaSelectProps, "onSelect" | "ref" | "value"> {
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
|
||||
export type SelectPortalContainer = HTMLDivElement | null
|
||||
|
||||
@@ -5,7 +5,7 @@ import styles from "./chevron.module.css"
|
||||
export default function SelectChevron() {
|
||||
return (
|
||||
<span aria-hidden="true" className={styles.chevron}>
|
||||
<ChevronDownIcon height={24} width={24} />
|
||||
<ChevronDownIcon color="grey80" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
|
||||
.activeSidebar {
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
background-color: var(--Scandic-Brand-Pale-Peach);
|
||||
}
|
||||
|
||||
.black {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const linkVariants = cva(styles.link, {
|
||||
color: {
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
none: "",
|
||||
pale: styles.pale,
|
||||
peach80: styles.peach80,
|
||||
},
|
||||
|
||||
98
components/TempDesignSystem/Select/index.tsx
Normal file
98
components/TempDesignSystem/Select/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
type Key,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
Select as ReactAriaSelect,
|
||||
SelectValue,
|
||||
} from "react-aria-components"
|
||||
|
||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import SelectChevron from "../Form/SelectChevron"
|
||||
|
||||
import styles from "./select.module.css"
|
||||
|
||||
import type {
|
||||
SelectPortalContainer,
|
||||
SelectPortalContainerArgs,
|
||||
SelectProps,
|
||||
} from "./select"
|
||||
|
||||
export default function Select({
|
||||
"aria-label": ariaLabel,
|
||||
defaultSelectedKey,
|
||||
items,
|
||||
label,
|
||||
name,
|
||||
onSelect,
|
||||
placeholder,
|
||||
value,
|
||||
}: SelectProps) {
|
||||
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined)
|
||||
|
||||
function setRef(node: SelectPortalContainerArgs) {
|
||||
if (node) {
|
||||
setRootDiv(node)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnSelect(key: Key) {
|
||||
onSelect(key)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={setRef}>
|
||||
<ReactAriaSelect
|
||||
aria-label={ariaLabel}
|
||||
className={styles.select}
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
name={name}
|
||||
onSelectionChange={handleOnSelect}
|
||||
placeholder={placeholder}
|
||||
selectedKey={value as Key}
|
||||
>
|
||||
<Body asChild fontOnly>
|
||||
<Button className={styles.input}>
|
||||
<div className={styles.inputContentWrapper}>
|
||||
<Label size="small">{label}</Label>
|
||||
<SelectValue />
|
||||
</div>
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
</Body>
|
||||
<Body asChild fontOnly>
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={rootDiv}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<ListBoxItem
|
||||
aria-label={String(item)}
|
||||
className={styles.listBoxItem}
|
||||
id={item.value}
|
||||
key={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Body>
|
||||
</ReactAriaSelect>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--Base-Text-UI-Placeholder);
|
||||
}
|
||||
|
||||
.select {
|
||||
border: 1px solid var(--Base-Border-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
@@ -27,7 +23,7 @@
|
||||
color: var(--Base-Text-UI-High-contrast);
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 56px;
|
||||
height: 60px;
|
||||
outline: none;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
text-align: left;
|
||||
15
components/TempDesignSystem/Select/select.ts
Normal file
15
components/TempDesignSystem/Select/select.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
export interface SelectProps
|
||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {
|
||||
defaultSelectedKey?: Key
|
||||
items: { label: string; value: Key }[]
|
||||
label: string
|
||||
name: string
|
||||
onSelect: (key: Key) => void
|
||||
placeholder?: string
|
||||
value?: string | number
|
||||
}
|
||||
|
||||
export type SelectPortalContainer = HTMLDivElement | undefined
|
||||
export type SelectPortalContainerArgs = HTMLDivElement | null
|
||||
@@ -5,18 +5,22 @@
|
||||
}
|
||||
|
||||
.one {
|
||||
font-size: clamp(var(--typography-Script-1-Mobile-fontSize),
|
||||
1.3vw + 14px,
|
||||
var(--typography-Script-1-Desktop-fontSize));
|
||||
font-size: clamp(
|
||||
var(--typography-Script-1-Mobile-fontSize),
|
||||
1.3vw + 14px,
|
||||
var(--typography-Script-1-Desktop-fontSize)
|
||||
);
|
||||
font-weight: var(--typography-Script-1-fontWeight);
|
||||
letter-spacing: var(--typography-Script-1-letterSpacing);
|
||||
line-height: var(--typography-Script-1-lineHeight);
|
||||
}
|
||||
|
||||
.two {
|
||||
font-size: clamp(var(--typography-Script-2-Mobile-fontSize),
|
||||
0.6vw + 15px,
|
||||
var(--typography-Script-2-Desktop-fontSize));
|
||||
font-size: clamp(
|
||||
var(--typography-Script-2-Mobile-fontSize),
|
||||
0.6vw + 15px,
|
||||
var(--typography-Script-2-Desktop-fontSize)
|
||||
);
|
||||
font-weight: var(--typography-Script-2-fontWeight);
|
||||
letter-spacing: var(--typography-Script-2-letterSpacing);
|
||||
line-height: var(--typography-Script-2-lineHeight);
|
||||
@@ -31,7 +35,7 @@
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000;
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
|
||||
@@ -43,8 +43,7 @@
|
||||
}
|
||||
|
||||
.black {
|
||||
/* No black variable exist yet */
|
||||
color: #000;
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
|
||||
@@ -26,8 +26,7 @@
|
||||
}
|
||||
|
||||
.black {
|
||||
/* No black variable exist yet */
|
||||
color: #000;
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
}
|
||||
|
||||
.black {
|
||||
/* No black variable exist yet */
|
||||
color: #000;
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from "./subtitle.module.css"
|
||||
const config = {
|
||||
variants: {
|
||||
color: {
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
},
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000;
|
||||
color: var(--Main-Grey-100);
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
@@ -96,6 +96,10 @@
|
||||
color: var(--Scandic-Brand-Pale-Peach);
|
||||
}
|
||||
|
||||
.peach80 {
|
||||
color: var(--Scandic-Peach-80);
|
||||
}
|
||||
|
||||
.red {
|
||||
color: var(--Scandic-Brand-Scandic-Red);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const config = {
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
peach80: styles.peach80,
|
||||
red: styles.red,
|
||||
},
|
||||
textAlign: {
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
"Continue": "Blive ved",
|
||||
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
|
||||
"Country": "Land",
|
||||
"Country code": "Landekode",
|
||||
"Current level": "Nuværende niveau",
|
||||
"Current password": "Nuværende kodeord",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivelse",
|
||||
"Discard changes": "Kassér ændringer",
|
||||
"Edit": "Redigere",
|
||||
"Edit profile": "Rediger profil",
|
||||
"Email": "E-mail",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Find booking": "Find booking",
|
||||
"Free soft drink voucher for the kids when staying": "Gratis sodavandskupon til børnene, når de bor",
|
||||
"Get inspired": "Blive inspireret",
|
||||
"Go back to overview": "Gå tilbage til oversigten",
|
||||
"How it works": "Hvordan det virker",
|
||||
@@ -45,6 +47,7 @@
|
||||
"My credit cards": "Mine kreditkort",
|
||||
"My pages": "Mine sider",
|
||||
"My wishes": "Mine ønsker",
|
||||
"New password": "Nyt kodeord",
|
||||
"Next": "Næste",
|
||||
"Next level": "Næste niveau",
|
||||
"No content published": "Intet indhold offentliggjort",
|
||||
@@ -66,10 +69,12 @@
|
||||
"Previous victories": "Tidligere sejre",
|
||||
"points until next level": "point indtil næste niveau",
|
||||
"Read more": "Læs mere",
|
||||
"Retype new password": "Gentag den nye adgangskode",
|
||||
"Save": "Gemme",
|
||||
"Select a country": "Vælg et land",
|
||||
"Select country of residence": "Vælg bopælsland",
|
||||
"Select date of birth": "Vælg fødselsdato",
|
||||
"Select language": "Vælg sprog",
|
||||
"Show more": "Vis mere",
|
||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||
"Something went wrong!": "Noget gik galt!",
|
||||
@@ -77,6 +82,7 @@
|
||||
"Total Points": "Samlet antal point",
|
||||
"Transaction date": "Overførselsdato",
|
||||
"Transactions": "Transaktioner",
|
||||
"User information": "Brugeroplysninger",
|
||||
"Visiting address": "Besøgsadresse",
|
||||
"Where should you go next?": "Hvor skal du tage hen næste gang?",
|
||||
"Year": "År",
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
"Continue": "Weitermachen",
|
||||
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
|
||||
"Country": "Land",
|
||||
"Country code": "Landesvorwahl",
|
||||
"Current level": "Aktuelles Level",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"Date of Birth": "Geburtsdatum",
|
||||
"Day": "Tag",
|
||||
"Description": "Beschreibung",
|
||||
"Discard changes": "Änderungen verwerfen",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit profile": "Profil bearbeiten",
|
||||
"Email": "Email",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Find booking": "Buchung finden",
|
||||
"Free soft drink voucher for the kids when staying": "Gutschein für einen kostenlosen Softdrink für die Kinder bei Aufenthalt",
|
||||
"Get inspired": "Lass dich inspirieren",
|
||||
"Go back to overview": "Zurück zur Übersicht",
|
||||
"How it works": "Wie es funktioniert",
|
||||
@@ -45,6 +47,7 @@
|
||||
"My credit cards": "Meine Kreditkarten",
|
||||
"My pages": "Meine Seiten",
|
||||
"My wishes": "Meine Wünsche",
|
||||
"New password": "Neues Kennwort",
|
||||
"Next": "Nächste",
|
||||
"Next level": "Nächste Ebene",
|
||||
"No content published": "Kein Inhalt veröffentlicht",
|
||||
@@ -66,10 +69,12 @@
|
||||
"Previous victories": "Bisherige Siege",
|
||||
"points until next level": "punkte bis zum nächsten Level",
|
||||
"Read more": "Mehr lesen",
|
||||
"Retype new password": "Neues Passwort erneut eingeben",
|
||||
"Save": "Speichern",
|
||||
"Select a country": "Wähle ein Land",
|
||||
"Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus",
|
||||
"Select date of birth": "Geburtsdatum auswählen",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Show more": "Zeig mehr",
|
||||
"Skip to main content": "Direkt zum Inhalt",
|
||||
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||
@@ -77,6 +82,7 @@
|
||||
"Total Points": "Gesamtpunktzahl",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktionen",
|
||||
"User information": "Nutzerinformation",
|
||||
"Visiting address": "Besuchsadresse",
|
||||
"Where should you go next?": "Wohin soll es als nächstes gehen?",
|
||||
"Year": "Jahr",
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
"Continue": "Continue",
|
||||
"Could not find requested resource": "Could not find requested resource",
|
||||
"Country": "Country",
|
||||
"Country code": "Country code",
|
||||
"Current level": "Current level",
|
||||
"Current password": "Current password",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Day": "Day",
|
||||
"Description": "Description",
|
||||
"Discard changes": "Discard changes",
|
||||
"Edit": "Edit",
|
||||
"Edit profile": "Edit profile",
|
||||
"Email": "Email",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Find booking": "Find booking",
|
||||
"Free soft drink voucher for the kids when staying": "Free soft drink voucher for the kids when staying",
|
||||
"Get inspired": "Get inspired",
|
||||
"Go back to overview": "Go back to overview",
|
||||
"How it works": "How it works",
|
||||
@@ -45,6 +47,7 @@
|
||||
"My credit cards": "My credit cards",
|
||||
"My pages": "My pages",
|
||||
"My wishes": "My wishes",
|
||||
"New password": "New password",
|
||||
"Next": "Next",
|
||||
"Next level": "Next level",
|
||||
"No content published": "No content published",
|
||||
@@ -66,10 +69,12 @@
|
||||
"Previous victories": "Previous victories",
|
||||
"points until next level": "points until next level",
|
||||
"Read more": "Read more",
|
||||
"Retype new password": "Retype new password",
|
||||
"Save": "Save",
|
||||
"Select a country": "Select a country",
|
||||
"Select country of residence": "Select country of residence",
|
||||
"Select date of birth": "Select date of birth",
|
||||
"Select language": "Select language",
|
||||
"Show more": "Show more",
|
||||
"Skip to main content": "Skip to main content",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
@@ -77,8 +82,9 @@
|
||||
"Total Points": "Total Points",
|
||||
"Transaction date": "Transaction date",
|
||||
"Transactions": "Transactions",
|
||||
"Where should you go next?": "Where should you go next?",
|
||||
"User information": "User information",
|
||||
"Visiting address": "Visiting address",
|
||||
"Where should you go next?": "Where should you go next?",
|
||||
"Year": "Year",
|
||||
"You have no previous stays.": "You have no previous stays.",
|
||||
"You have no upcoming stays.": "You have no upcoming stays.",
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
"Continue": "Jatkaa",
|
||||
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
|
||||
"Country": "Maa",
|
||||
"Country code": "Maatunnus",
|
||||
"Current level": "Nykyinen taso",
|
||||
"Current password": "Nykyinen salasana",
|
||||
"Date of Birth": "Syntymäaika",
|
||||
"Day": "Päivä",
|
||||
"Description": "Kuvaus",
|
||||
"Discard changes": "Hylkää muutokset",
|
||||
"Edit": "Muokata",
|
||||
"Edit profile": "Muokkaa profiilia",
|
||||
"Email": "Sähköposti",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Free soft drink voucher for the kids when staying": "Ilmainen virvoitusjuomakuponki lapsille majoittuessaan",
|
||||
"Get inspired": "Inspiroidu",
|
||||
"Go back to overview": "Palaa yleiskatsaukseen",
|
||||
"How it works": "Kuinka se toimii",
|
||||
@@ -45,6 +47,7 @@
|
||||
"My credit cards": "Minun luottokorttini",
|
||||
"My pages": "Omat sivut",
|
||||
"My wishes": "Toiveeni",
|
||||
"New password": "Uusi salasana",
|
||||
"Next": "Seuraava",
|
||||
"Next level": "Seuraava taso",
|
||||
"No content published": "Ei julkaistua sisältöä",
|
||||
@@ -66,10 +69,12 @@
|
||||
"Previous victories": "Edelliset voitot",
|
||||
"points until next level": "pisteitä seuraavalle tasolle",
|
||||
"Read more": "Lue lisää",
|
||||
"Retype new password": "Kirjoita uusi salasana uudelleen",
|
||||
"Save": "Tallentaa",
|
||||
"Select a country": "Valitse maa",
|
||||
"Select country of residence": "Valitse asuinmaa",
|
||||
"Select date of birth": "Valitse syntymäaika",
|
||||
"Select language": "Valitse kieli",
|
||||
"Show more": "Näytä lisää",
|
||||
"Skip to main content": "Siirry pääsisältöön",
|
||||
"Something went wrong!": "Jotain meni pieleen!",
|
||||
@@ -77,6 +82,7 @@
|
||||
"Total Points": "Kokonaispisteet",
|
||||
"Transaction date": "Tapahtuman päivämäärä",
|
||||
"Transactions": "Tapahtumat",
|
||||
"User information": "Käyttäjän tiedot",
|
||||
"Visiting address": "Käyntiosoite",
|
||||
"Where should you go next?": "Minne sinun pitäisi mennä seuraavaksi?",
|
||||
"Year": "Vuosi",
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
"Continue": "Fortsette",
|
||||
"Could not find requested resource": "Kunne ikke finne den forespurte ressursen",
|
||||
"Country": "Land",
|
||||
"Country code": "Landskode",
|
||||
"Current level": "Nåværende nivå",
|
||||
"Current password": "Nåværende passord",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivelse",
|
||||
"Discard changes": "Forkaste endringer",
|
||||
"Edit": "Redigere",
|
||||
"Edit profile": "Rediger profil",
|
||||
"Email": "E-post",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Find booking": "Finn booking",
|
||||
"Free soft drink voucher for the kids when staying": "Gratis bruskupong for barna når de bor",
|
||||
"Get inspired": "Bli inspirert",
|
||||
"Go back to overview": "Gå tilbake til oversikten",
|
||||
"How it works": "Hvordan det fungerer",
|
||||
@@ -45,6 +47,7 @@
|
||||
"My credit cards": "Kredittkortene mine",
|
||||
"My pages": "Mine sider",
|
||||
"My wishes": "Mine ønsker",
|
||||
"New password": "Nytt passord",
|
||||
"Next": "Neste",
|
||||
"Next level": "Neste nivå",
|
||||
"No content published": "Ingen innhold publisert",
|
||||
@@ -66,10 +69,12 @@
|
||||
"Previous victories": "Tidligere seire",
|
||||
"points until next level": "poeng til neste nivå",
|
||||
"Read more": "Les mer",
|
||||
"Retype new password": "Skriv inn nytt passord på nytt",
|
||||
"Save": "Lagre",
|
||||
"Select a country": "Velg et land",
|
||||
"Select country of residence": "Velg bostedsland",
|
||||
"Select date of birth": "Velg fødselsdato",
|
||||
"Select language": "Velg språk",
|
||||
"Show more": "Vis mer",
|
||||
"Skip to main content": "Gå videre til hovedsiden",
|
||||
"Something went wrong!": "Noe gikk galt!",
|
||||
@@ -77,6 +82,7 @@
|
||||
"Total Points": "Totale poeng",
|
||||
"Transaction date": "Transaksjonsdato",
|
||||
"Transactions": "Transaksjoner",
|
||||
"User information": "Brukerinformasjon",
|
||||
"Visiting address": "Besøksadresse",
|
||||
"Where should you go next?": "Hvor bør du gå videre?",
|
||||
"Year": "År",
|
||||
|
||||
@@ -18,17 +18,19 @@
|
||||
"Continue": "Fortsätt",
|
||||
"Could not find requested resource": "Det gick inte att hitta den begärda resursen",
|
||||
"Country": "Land",
|
||||
"Country code": "Landskod",
|
||||
"Current level": "Nuvarande nivå",
|
||||
"Current password": "Nuvarande lösenord",
|
||||
"Date of Birth": "Födelsedatum",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivning",
|
||||
"Discard changes": "Ignorera ändringar",
|
||||
"Edit": "Redigera",
|
||||
"Edit profile": "Redigera profil",
|
||||
"Email": "E-post",
|
||||
"Empty": "Tom",
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Free soft drink voucher for the kids when staying": "Gratis läskkupong för barnen när de bor",
|
||||
"Get inspired": "Bli inspirerad",
|
||||
"Go back to overview": "Gå tillbaka till översikten",
|
||||
"How it works": "Hur det fungerar",
|
||||
@@ -45,6 +47,7 @@
|
||||
"My credit cards": "Mina kreditkort",
|
||||
"My pages": "Mina sidor",
|
||||
"My wishes": "Mina önskningar",
|
||||
"New password": "Nytt lösenord",
|
||||
"Next": "Nästa",
|
||||
"Next level": "Nästa nivå",
|
||||
"No content published": "Inget innehåll publicerat",
|
||||
@@ -66,10 +69,12 @@
|
||||
"Previous victories": "Tidigare segrar",
|
||||
"points until next level": "poäng till nästa nivå",
|
||||
"Read more": "Läs mer",
|
||||
"Retype new password": "Upprepa nytt lösenord",
|
||||
"Save": "Spara",
|
||||
"Select a country": "Välj ett land",
|
||||
"Select country of residence": "Välj bosättningsland",
|
||||
"Select date of birth": "Välj födelsedatum",
|
||||
"Select language": "Välj språk",
|
||||
"Show more": "Visa mer",
|
||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||
"Something went wrong!": "Något gick fel!",
|
||||
@@ -77,6 +82,7 @@
|
||||
"Total Points": "Total poäng",
|
||||
"Transaction date": "Transaktionsdatum",
|
||||
"Transactions": "Transaktioner",
|
||||
"User information": "Användar information",
|
||||
"Visiting address": "Besöksadress",
|
||||
"Where should you go next?": "Vart ska du gå härnäst?",
|
||||
"Year": "År",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countri
|
||||
export const getUserSchema = z.object({
|
||||
address: z.object({
|
||||
city: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
countryCode: z.nativeEnum(countriesMap).optional(),
|
||||
streetAddress: z.string().optional(),
|
||||
zipCode: z.string(),
|
||||
|
||||
@@ -56,8 +56,6 @@ export const userQueryRouter = router({
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
console.log({ apiJson })
|
||||
console.log({ attr: apiJson.data.attributes })
|
||||
if (!apiJson.data?.attributes) {
|
||||
// throw notFound(apiJson)
|
||||
console.error(
|
||||
@@ -78,12 +76,14 @@ export const userQueryRouter = router({
|
||||
const country = countries.find(
|
||||
(c) => c.code === verifiedData.data.address.countryCode
|
||||
)
|
||||
const phonenumber = parsePhoneNumber(verifiedData.data.phoneNumber)
|
||||
|
||||
const user = {
|
||||
...extendedUser,
|
||||
address: {
|
||||
city: verifiedData.data.address.city,
|
||||
country: country?.name ?? verifiedData.data.address.countryCode,
|
||||
country: country?.name ?? "",
|
||||
countryCode: verifiedData.data.address.countryCode,
|
||||
streetAddress: verifiedData.data.address.streetAddress,
|
||||
zipCode: verifiedData.data.address.zipCode,
|
||||
},
|
||||
@@ -94,7 +94,7 @@ export const userQueryRouter = router({
|
||||
lastName: verifiedData.data.lastName,
|
||||
memberships: verifiedData.data.memberships,
|
||||
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
|
||||
phoneNumber: verifiedData.data.phoneNumber,
|
||||
phoneNumber: phonenumber.formatInternational(),
|
||||
profileId: verifiedData.data.profileId,
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ export const userQueryRouter = router({
|
||||
user.address.zipCode = maskValue.text(verifiedData.data.address.zipCode)
|
||||
user.email = maskValue.email(user.email)
|
||||
|
||||
const phonenumber = parsePhoneNumber(user.phoneNumber)
|
||||
user.phoneNumber = `+${phonenumber.countryCallingCode} ${maskValue.phone(user.phoneNumber)}`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
interface EditProfileState {
|
||||
pending: boolean
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
interface EditProfileActions {
|
||||
setIsPending: (isPending: boolean) => void
|
||||
setValid: (isValid: boolean) => void
|
||||
}
|
||||
|
||||
export interface EditProfileStore
|
||||
extends EditProfileActions,
|
||||
EditProfileState { }
|
||||
|
||||
export const useProfileStore = create<EditProfileStore>()((set) => ({
|
||||
pending: false,
|
||||
valid: true,
|
||||
|
||||
setIsPending: (isPending) => set(() => ({ pending: isPending })),
|
||||
setValid: (isValid) => set(() => ({ valid: isValid })),
|
||||
}))
|
||||
@@ -1,15 +1,9 @@
|
||||
import type { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||
import type { Control } from "react-hook-form"
|
||||
import type { User } from "@/types/user"
|
||||
|
||||
export type EditFormProps = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export type EditFormContentProps = {
|
||||
control: Control<EditProfileSchema>
|
||||
}
|
||||
|
||||
type E = {
|
||||
message: string
|
||||
path: string
|
||||
|
||||
Reference in New Issue
Block a user