feat(WEB-162): final design edit profile page

This commit is contained in:
Simon Emanuelsson
2024-06-18 08:15:57 +02:00
committed by Christel Westerberg
parent 5f3e417593
commit d84efcbb0f
81 changed files with 1538 additions and 711 deletions

View File

@@ -1,13 +1,13 @@
"use server" "use server"
import { editProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema" // import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
import { ZodError } from "zod" import { ZodError } from "zod"
import { type State, Status } from "@/types/components/myPages/myProfile/edit" import { type State, Status } from "@/types/components/myPages/myProfile/edit"
export async function editProfile(_prevState: State, values: FormData) { export async function editProfile(_prevState: State, values: FormData) {
try { try {
const data = editProfileSchema.parse(Object.fromEntries(values.entries())) const data = Object.fromEntries(values.entries())
/** /**
* TODO: Update profile data when endpoint from * TODO: Update profile data when endpoint from

View File

@@ -11,19 +11,19 @@ export default async function CommunicationSlot() {
return ( return (
<section className={styles.container}> <section className={styles.container}>
<article className={styles.content}> <article className={styles.content}>
<Subtitle> <Subtitle color="black">
{formatMessage({ id: "My communication preferences" })} {formatMessage({ id: "My communication preferences" })}
</Subtitle> </Subtitle>
<Body> <Body color="black">
{formatMessage({ {formatMessage({
id: "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", id: "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
})} })}
</Body> </Body>
</article> </article>
<Link href="#" variant="icon"> <Link href="#" variant="icon">
<ArrowRightIcon color="red" /> <ArrowRightIcon color="burgundy" />
<Body color="red" textTransform="underlined"> <Body color="burgundy" textTransform="underlined">
{formatMessage({ id: "Add new card" })} {formatMessage({ id: "Manage preferences" })}
</Body> </Body>
</Link> </Link>
</section> </section>

View File

@@ -11,16 +11,18 @@ export default async function CreditCardSlot() {
return ( return (
<section className={styles.container}> <section className={styles.container}>
<article className={styles.content}> <article className={styles.content}>
<Subtitle>{formatMessage({ id: "My credit cards" })}</Subtitle> <Subtitle color="black">
<Body> {formatMessage({ id: "My credit cards" })}
</Subtitle>
<Body color="black">
{formatMessage({ {formatMessage({
id: "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", id: "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
})} })}
</Body> </Body>
</article> </article>
<Link href="#" variant="icon"> <Link href="#" variant="icon">
<PlusCircleIcon color="red" /> <PlusCircleIcon color="burgundy" />
<Body color="red" textTransform="underlined"> <Body color="burgundy" textTransform="underlined">
{formatMessage({ id: "Add new card" })} {formatMessage({ id: "Add new card" })}
</Body> </Body>
</Link> </Link>

View File

@@ -1,3 +1,3 @@
export default function Default() { export default function DefaultEditProfileSlot() {
return null return null
} }

View File

@@ -1,5 +1,11 @@
import EditProfile from "@/components/MyProfile/Profile/Edit" import { serverClient } from "@/lib/trpc/server"
export default function EditProfileSlot() { import Form from "@/components/Forms/Edit/Profile"
return <EditProfile />
export default async function EditProfileSlot() {
const user = await serverClient().user.get({ mask: false })
if (!user) {
return null
}
return <Form user={user} />
} }

View File

@@ -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);
}

View File

@@ -0,0 +1,7 @@
import styles from "./layout.module.css"
export default function ProfileSlotLayout({
children,
}: React.PropsWithChildren) {
return <section className={styles.container}>{children}</section>
}

View File

@@ -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 { .profile {
display: flex; display: flex;
gap: var(--Spacing-x2); gap: var(--Spacing-x2);

View File

@@ -1,4 +1,4 @@
// import { dt } from "@/lib/dt" import { profileEdit } from "@/constants/routes/myPages"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { import {
@@ -8,36 +8,41 @@ import {
LockIcon, LockIcon,
PhoneIcon, PhoneIcon,
} from "@/components/Icons" } from "@/components/Icons"
import Header from "@/components/Profile/Header"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider" import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import styles from "./page.module.css" 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 { formatMessage } = await getIntl()
const user = await serverClient().user.get() const user = await serverClient().user.get()
if (!user) { if (!user) {
return null return null
} }
// const dob = dt(user.dateOfBirth).format("DD/MM/YYYY")
return ( return (
<section className={styles.container}> <>
<header className={styles.header}> <Header>
<hgroup> <hgroup>
<Title as="h4" color="red" level="h1"> <Title as="h4" color="peach80" level="h1">
{user.name} {formatMessage({ id: "Welcome" })}
</Title> </Title>
<Title as="h4" color="burgundy" level="h2"> <Title as="h4" color="burgundy" level="h2">
{user.dateOfBirth} {user.name}
</Title> </Title>
</hgroup> </hgroup>
<Button className={styles.btn} size="large" theme="primaryStrong"> <Button asChild intent="secondary" size="small" theme="base">
{formatMessage({ id: "Edit profile" })} <Link color="none" href={profileEdit[params.lang]}>
{formatMessage({ id: "Edit profile" })}
</Link>
</Button> </Button>
</header> </Header>
<Divider color="burgundy" opacity={8} /> <Divider color="burgundy" opacity={8} />
<section className={styles.profile}> <section className={styles.profile}>
<article className={styles.info}> <article className={styles.info}>
@@ -91,6 +96,6 @@ export default async function Profile() {
</div> </div>
</aside> </aside>
</section> </section>
</section> </>
) )
} }

View File

@@ -0,0 +1,5 @@
import "../profileLayout.css"
export default function EditProfilePage() {
return null
}

View File

@@ -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} />
</>
)
}

View File

@@ -1,3 +0,0 @@
export default function DefaultLanguageSwitcher() {
return null
}

View File

@@ -1,3 +1 @@
export { GET, POST } from "@/auth" export { GET, POST } from "@/auth"
export const runtime = "edge"

View File

@@ -72,14 +72,6 @@
src: url(/_static/fonts/fira-sans/medium.woff2) format("woff2"); 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-face {
font-display: swap; font-display: swap;
font-family: "fira sans"; font-family: "fira sans";

View File

@@ -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;
}

View 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>
</>
)
}

View 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);
}

View 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>
)
}

View 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>

View File

@@ -12,27 +12,27 @@ export default function ChevronDownIcon({
<svg <svg
className={classNames} className={classNames}
fill="none" fill="none"
height="24" height="20"
viewBox="0 0 24 24" viewBox="0 0 20 20"
width="24" width="20"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<mask <mask
height="24" height="20"
id="mask0_553_6963" id="mask0_4971_13121"
maskUnits="userSpaceOnUse" maskUnits="userSpaceOnUse"
style={{ maskType: "alpha" }} style={{ maskType: "alpha" }}
width="24" width="20"
x="0" x="0"
y="0" y="0"
> >
<rect width="24" height="24" fill="#D9D9D9" /> <rect width="20" height="20" fill="#D9D9D9" />
</mask> </mask>
<g mask="url(#mask0_553_6963)"> <g mask="url(#mask0_4971_13121)">
<path <path
d="M12 15.3746L6 9.37461L7.4 7.97461L12 12.5746L16.6 7.97461L18 9.37461L12 15.3746Z" 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="#757575" fill="#4D001B"
/> />
</g> </g>
</svg> </svg>

View 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>
)
}

View File

@@ -1,6 +1,5 @@
.icon { .icon {
height: 20px; margin: 0;
width: 20px;
} }
.black, .black,
@@ -13,6 +12,11 @@
fill: var(--Scandic-Brand-Burgundy); fill: var(--Scandic-Brand-Burgundy);
} }
.grey80,
.grey80 * {
fill: var(--UI-Grey-80);
}
.pale, .pale,
.pale * { .pale * {
fill: var(--Scandic-Brand-Pale-Peach); fill: var(--Scandic-Brand-Pale-Peach);

View File

@@ -9,6 +9,7 @@ export { default as ChevronRightIcon } from "./ChevronRight"
export { default as EmailIcon } from "./Email" export { default as EmailIcon } from "./Email"
export { default as GlobeIcon } from "./Globe" export { default as GlobeIcon } from "./Globe"
export { default as HouseIcon } from "./House" export { default as HouseIcon } from "./House"
export { default as InfoCircleIcon } from "./InfoCircle"
export { default as LocationIcon } from "./Location" export { default as LocationIcon } from "./Location"
export { default as LockIcon } from "./Lock" export { default as LockIcon } from "./Lock"
export { default as PhoneIcon } from "./Phone" export { default as PhoneIcon } from "./Phone"

View File

@@ -7,6 +7,7 @@ const config = {
color: { color: {
black: styles.black, black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
grey80: styles.grey80,
pale: styles.pale, pale: styles.pale,
peach80: styles.peach80, peach80: styles.peach80,
primaryLightOnSurfaceAccent: styles.plosa, primaryLightOnSurfaceAccent: styles.plosa,

View File

@@ -8,7 +8,7 @@ import { Lang } from "@/constants/languages"
import { membershipLevels } from "@/constants/membershipLevels" import { membershipLevels } from "@/constants/membershipLevels"
import Image from "@/components/Image" import Image from "@/components/Image"
import Select from "@/components/TempDesignSystem/Form/Select" import Select from "@/components/TempDesignSystem/Select"
import { getMembership } from "@/utils/user" import { getMembership } from "@/utils/user"
import levelsData from "./data/EN.json" import levelsData from "./data/EN.json"

View File

@@ -7,7 +7,7 @@ import MobileTable from "./Mobile"
import type { import type {
ClientEarnAndBurnProps, ClientEarnAndBurnProps,
TransactionsObject, TransactionsNonNullResponseObject,
} from "@/types/components/myPages/myPage/earnAndBurn" } from "@/types/components/myPages/myPage/earnAndBurn"
export default function ClientEarnAndBurn({ export default function ClientEarnAndBurn({
@@ -34,7 +34,7 @@ export default function ClientEarnAndBurn({
// later on when we handle errors appropriately. // later on when we handle errors appropriately.
const filteredTransactions = (data?.pages.filter( const filteredTransactions = (data?.pages.filter(
(page) => page && page.data (page) => page && page.data
) ?? []) as unknown as TransactionsObject[] ) ?? []) as unknown as TransactionsNonNullResponseObject[]
const transactions = filteredTransactions.flatMap((page) => page.data) const transactions = filteredTransactions.flatMap((page) => page.data)
return ( return (
<> <>

View File

@@ -56,13 +56,11 @@ export default function ClientPreviousStays({
stay={stay} stay={stay}
/> />
))} ))}
</Grids.Stackable > </Grids.Stackable>
{ {hasNextPage ? (
hasNextPage ? ( <ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} /> ) : null}
) : null </ListContainer>
}
</ListContainer >
) : ( ) : (
<EmptyPreviousStaysBlock /> <EmptyPreviousStaysBlock />
) )

View File

@@ -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 <></>
}

View File

@@ -1,5 +0,0 @@
.form {
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: 1fr 1fr;
}

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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} />
}

View File

@@ -0,0 +1,6 @@
.header {
align-items: center;
display: flex;
gap: var(--Spacing-x2);
justify-content: space-between;
}

View 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>
}

View File

@@ -1,167 +1,411 @@
.btn { .btn {
background: none; background: none;
border: none; /* No variable yet for radius 50px */
border-radius: 50px;
cursor: pointer; 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; margin: 0;
padding: 0; padding: 0;
text-align: center; 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, .primary,
a.primary { a.primary {
background-color: var(--background-color); border: none;
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);
} }
.secondary, .secondary,
a.secondary { a.secondary {
background-color: transparent; background: none;
border: 1px solid var(--background-color); border-style: solid;
color: var(--background-color); border-width: 2px;
} }
.secondary:hover, .tertiary,
a.secondary:hover, a.tertiary {
.secondary:active, border: none;
a.secondary:active,
.secondary:focus,
a.secondary:focus {
border: 1px solid var(--hover-color);
color: var(--hover-color);
} }
.inverted,
a.inverted {
border: none;
}
/* VARIANTS */
.default, .default,
a.default { a.default {
align-items: center; align-items: center;
border-radius: 50px;
/* No variable yet */
display: flex; display: flex;
gap: var(--Spacing-x1); gap: var(--Spacing-x1);
} }
.icon { .icon {
align-items: baseline; 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 { .btn:disabled {
background-color: var(--disabled-background-color); background-color: var(--disabled-background-color);
color: var(--disabled-color); color: var(--disabled-color);
cursor: not-allowed; cursor: not-allowed;
} }
/* Sizes */ /* THEMES */
.small { .basePrimary {
font-size: 14px; background-color: var(--Base-Button-Primary-Fill-Normal);
gap: var(--Spacing-x2); color: var(--Base-Button-Primary-On-Fill-Normal);
height: 40px;
padding: var(--Spacing-x1) var(--Spacing-x2);
} }
.medium { .basePrimary:active,
font-size: 16px; .basePrimary:focus,
height: 30px; .basePrimary:hover {
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); background-color: var(--Base-Button-Primary-Fill-Hover);
color: var(--Base-Button-Primary-On-Fill-Hover);
} }
.large { .basePrimary:disabled {
font-size: 16px; background-color: var(--Base-Button-Primary-Fill-Disabled);
height: 50px; color: var(--Base-Button-Primary-On-Fill-Disabled);
gap: var(--Spacing-x-half);
padding: var(--Spacing-x2) var(--Spacing-x3);
} }
.primaryLight { .baseSecondary {
--font-color: var(--Theme-Primary-Light-Button-Primary-On-Fill-Normal); background-color: var(--Base-Button-Secondary-Fill-Normal);
--background-color: var(--Theme-Primary-Light-Button-Primary-Fill-Normal); border-color: var(--Base-Button-Secondary-Border-Normal);
--hover-background: var(--Theme-Primary-Light-Button-Primary-Fill-Hover); color: var(--Base-Button-Secondary-On-Fill-Normal);
--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);
} }
.primaryDark { .baseSecondary:active,
--font-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Normal); .baseSecondary:focus,
--background-color: var(--Theme-Primary-Dark-Button-Primary-Fill-Normal); .baseSecondary:hover {
--hover-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Hover); background-color: var(--Base-Button-Secondary-Fill-Hover);
--hover-background: var(--Theme-Primary-Dark-Button-Primary-Fill-Hover); border-color: var(--Base-Button-Secondary-Border-Hover);
--disabled-background-color: var( color: var(--Base-Button-Secondary-On-Fill-Hover);
--Theme-Primary-Dark-Button-Primary-Fill-Disabled
);
--disabled-color: var(--Theme-Primary-Dark-Button-Primary-On-Fill-Disabled);
} }
.primaryStrong { .baseSecondary:disabled {
--background-color: var(--Theme-Primary-Strong-Button-Primary-Fill-Normal); background-color: var(--Base-Button-Secondary-Fill-Disabled);
--disabled-background-color: var( border-color: var(--Base-Button-Secondary-Border-Disabled);
--Theme-Primary-Strong-Button-Primary-Fill-Disabled color: var(--Base-Button-Secondary-On-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);
} }
.secondaryLight { .baseTertiary {
--font-color: var(--Theme-Secondary-Light-Button-Primary-On-Fill-Normal); background-color: var(--Base-Button-Tertiary-Fill-Normal);
--background-color: var(--Theme-Secondary-Light-Button-Primary-Fill-Normal); color: var(--Base-Button-Tertiary-On-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
);
} }
.secondaryDark { .baseTertiary:active,
--font-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Normal); .baseTertiary:focus,
--background-color: var(--Theme-Secondary-Dark-Button-Primary-Fill-Normal); .baseTertiary:hover {
--hover-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Hover); background-color: var(--Base-Button-Tertiary-Fill-Hover);
--hover-background: var(--Theme-Secondary-Dark-Button-Primary-Fill-Hover); color: var(--Base-Button-Tertiary-On-Fill-Hover);
--disabled-background-color: var(
--Theme-Secondary-Dark-Button-Primary-Fill-Disabled
);
--disabled-color: var(--Theme-Secondary-Dark-Button-Primary-On-Fill-Disabled);
} }
.tertiaryLight { .baseTertiary:disabled {
--font-color: var(--Theme-Tertiary-Light-Button-Primary-On-Fill-Normal); background-color: var(--Base-Button-Tertiary-Fill-Disabled);
--background-color: var(--Theme-Tertiary-Light-Button-Primary-Fill-Normal); color: var(--Base-Button-Tertiary-On-Fill-Disabled);
--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);
} }
.tertiaryDark { .baseInverted {
--font-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Normal); background-color: var(--Base-Button-Inverted-Fill-Normal);
--background-color: var(--Theme-Tertiary-Dark-Button-Primary-Fill-Normal); color: var(--Base-Button-Inverted-On-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( .baseInverted:active,
--Theme-Tertiary-Dark-Button-Primary-Fill-Disabled .baseInverted:focus,
); .baseInverted:hover {
--disabled-color: var(--Theme-Tertiary-Dark-Button-Primary-On-Fill-Disabled); 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);
} }

View File

@@ -5,8 +5,10 @@ import styles from "./button.module.css"
export const buttonVariants = cva(styles.btn, { export const buttonVariants = cva(styles.btn, {
variants: { variants: {
intent: { intent: {
inverted: styles.inverted,
primary: styles.primary, primary: styles.primary,
secondary: styles.secondary, secondary: styles.secondary,
tertiary: styles.tertiary,
}, },
size: { size: {
small: styles.small, small: styles.small,
@@ -14,13 +16,14 @@ export const buttonVariants = cva(styles.btn, {
large: styles.large, large: styles.large,
}, },
theme: { theme: {
primaryDark: styles.primaryDark, base: "",
primaryLight: styles.primaryLight, primaryDark: "",
primaryStrong: styles.primaryStrong, primaryStrong: "",
secondaryLight: styles.secondaryLight, primaryLight: "",
secondaryDark: styles.secondaryDark, secondaryLight: "",
tertiaryLight: styles.tertiaryLight, secondaryDark: "",
tertiaryDark: styles.tertiaryDark, tertiaryLight: "",
tertiaryDark: "",
}, },
variant: { variant: {
default: styles.default, default: styles.default,
@@ -33,4 +36,97 @@ export const buttonVariants = cva(styles.btn, {
theme: "primaryLight", theme: "primaryLight",
variant: "default", 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",
},
],
}) })

View File

@@ -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;
}
}

View File

@@ -1,35 +1,46 @@
.container { .container {
--select-border: 2px solid var(--UI-Grey-60); position: relative;
--select-width: min(28rem, 100%);
} }
.comboBoxContainer { .comboBoxContainer {
background-color: var(--Main-Grey-White); 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; display: grid;
grid-template-areas: "content"; gap: var(--Spacing-x-half);
width: var(--select-width); 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 { .input {
background-color: var(--Main-Grey-White); background-color: var(--Main-Grey-White);
border: var(--select-border); border: none;
border-radius: var(--Corner-radius-Small); grid-area: input;
grid-area: content; height: 18px;
height: 40px; padding: 0;
padding: var(--Spacing-x1) var(--Spacing-x2);
width: var(--select-width);
} }
.input, .input,
.listBoxItem { .listBoxItem {
color: var(--UI-Grey-60); color: var(--Main-Grey-100);
} }
.button { .button {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
grid-area: content; grid-area: chevron;
height: 100%; height: 100%;
justify-self: flex-end; justify-self: flex-end;
padding-left: 0; padding-left: 0;
@@ -38,13 +49,27 @@
.popover { .popover {
background-color: var(--Main-Grey-White); background-color: var(--Main-Grey-White);
border: var(--select-border); border-color: var(--Scandic-Beige-40);
border-radius: var(--Corner-radius-Small); border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Medium);
left: 0px;
max-height: 400px;
overflow: auto; overflow: auto;
padding: var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x2) var(--Spacing-x1); padding: var(--Spacing-x2);
width: var(--select-width); top: calc(60px + var(--Spacing-x1));
width: 100%;
} }
.listBoxItem { .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;
}

View File

@@ -1,6 +1,11 @@
import type { RegisterOptions } from "react-hook-form" import type { RegisterOptions } from "react-hook-form"
export type CountryProps = { export type CountryProps = {
label: string
name?: string name?: string
placeholder?: string
registerOptions?: RegisterOptions registerOptions?: RegisterOptions
} }
export type CountryPortalContainer = HTMLDivElement | undefined
export type CountryPortalContainerArgs = HTMLDivElement | null

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { ErrorMessage } from "@hookform/error-message" import { ErrorMessage } from "@hookform/error-message"
import { useRef } from "react" import { useState } from "react"
import { import {
Button, Button,
ComboBox, ComboBox,
@@ -14,21 +14,33 @@ import {
import { useController, useFormContext } from "react-hook-form" import { useController, useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" 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 Body from "@/components/TempDesignSystem/Text/Body"
import SelectChevron from "../SelectChevron"
import { countries } from "./countries" import { countries } from "./countries"
import styles from "./country.module.css" import styles from "./country.module.css"
import type { CountryProps } from "./country" import type {
CountryPortalContainer,
CountryPortalContainerArgs,
CountryProps,
} from "./country"
export default function CountrySelect({ export default function CountrySelect({
label,
name = "country", name = "country",
registerOptions, registerOptions = {},
}: CountryProps) { }: CountryProps) {
const { formatMessage } = useIntl() 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 { control, setValue } = useFormContext()
const { field } = useController({ const { field } = useController({
control, control,
@@ -43,7 +55,7 @@ export default function CountrySelect({
const selectCountryLabel = formatMessage({ id: "Select a country" }) const selectCountryLabel = formatMessage({ id: "Select a country" })
return ( return (
<div className={styles.container} ref={divRef}> <div className={styles.container} ref={setRef}>
<ComboBox <ComboBox
aria-label={formatMessage({ id: "Select country of residence" })} aria-label={formatMessage({ id: "Select country of residence" })}
className={styles.select} className={styles.select}
@@ -55,6 +67,13 @@ export default function CountrySelect({
selectedKey={field.value} selectedKey={field.value}
> >
<div className={styles.comboBoxContainer}> <div className={styles.comboBoxContainer}>
<Label
className={styles.label}
size="small"
required={!!registerOptions.required}
>
{label}
</Label>
<Body asChild fontOnly> <Body asChild fontOnly>
<Input <Input
aria-label={selectCountryLabel} aria-label={selectCountryLabel}
@@ -73,13 +92,14 @@ export default function CountrySelect({
className={styles.popover} className={styles.popover}
placement="bottom" placement="bottom"
shouldFlip={false} shouldFlip={false}
shouldUpdatePosition={false}
/** /**
* react-aria uses portals to render Popover in body * react-aria uses portals to render Popover in body
* unless otherwise specified. We need it to be contained * unless otherwise specified. We need it to be contained
* by this component to both access css variables assigned * by this component to both access css variables assigned
* on the container as well as to not overflow it at any time. * on the container as well as to not overflow it at any time.
*/ */
UNSTABLE_portalContainer={divRef.current ?? undefined} UNSTABLE_portalContainer={rootDiv}
> >
<ListBox> <ListBox>
{countries.map((country, idx) => ( {countries.map((country, idx) => (

View File

@@ -1,20 +1,9 @@
/* Leaving, will most likely get deleted */ /* Leaving, will most likely get deleted */
.container { .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; display: grid;
gap: 0.8rem; gap: var(--Spacing-x2);
grid-template-areas: "day month year"; grid-template-areas: "year month day";
grid-template-columns: min(--width-day, 1fr) min(--width-month, 1fr) min( grid-template-columns: 1fr 1fr 1fr;
--width-year,
2fr
);
width: var(--width); width: var(--width);
} }

View File

@@ -1,6 +1,4 @@
import type { Control, RegisterOptions } from "react-hook-form" import type { RegisterOptions } from "react-hook-form"
import type { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
export const enum DateName { export const enum DateName {
date = "date", date = "date",
@@ -9,7 +7,6 @@ export const enum DateName {
} }
export interface DateProps export interface DateProps
extends React.SelectHTMLAttributes<HTMLSelectElement> { extends React.SelectHTMLAttributes<HTMLSelectElement> {
control: Control<EditProfileSchema> name: string
name: keyof EditProfileSchema registerOptions: RegisterOptions
registerOptions: RegisterOptions<EditProfileSchema>
} }

View File

@@ -11,9 +11,9 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import Select from "@/components/TempDesignSystem/Select"
import { rangeArray } from "@/utils/rangeArray" import { rangeArray } from "@/utils/rangeArray"
import Select from "../Select"
import { DateName } from "./date" import { DateName } from "./date"
import styles from "./date.module.css" import styles from "./date.module.css"
@@ -23,14 +23,10 @@ import type { Key } from "react-aria-components"
import type { DateProps } from "./date" import type { DateProps } from "./date"
/** TODO: Get selecting with Enter-key to work */ /** TODO: Get selecting with Enter-key to work */
export default function DateSelect({ export default function DateSelect({ name, registerOptions }: DateProps) {
control,
name,
registerOptions,
}: DateProps) {
const { formatMessage } = useIntl() const { formatMessage } = useIntl()
const d = useWatch({ name }) const d = useWatch({ name })
const { setValue } = useFormContext() const { control, setValue } = useFormContext()
const { field } = useController({ const { field } = useController({
control, control,
name, name,

View File

@@ -1,4 +1,7 @@
.message { .message {
align-items: center;
color: var(--Scandic-Red-60); 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;
}

View File

@@ -1,6 +1,7 @@
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message" 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" import styles from "./error.module.css"
@@ -15,9 +16,10 @@ export default function ErrorMessage<T>({
errors={errors} errors={errors}
name={name} name={name}
render={({ message }) => ( render={({ message }) => (
<Body className={styles.message} fontOnly> <Caption className={styles.message} fontOnly>
<InfoCircleIcon color="red" />
{message} {message}
</Body> </Caption>
)} )}
/> />
) )

View File

@@ -1,8 +1,13 @@
"use client" "use client"
import { Input as AriaInput, TextField } from "react-aria-components" import {
import { useController } from "react-hook-form" 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 ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
import Label from "@/components/TempDesignSystem/Form/Label"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import styles from "./input.module.css" import styles from "./input.module.css"
@@ -11,17 +16,24 @@ import type { InputProps } from "./input"
export default function Input({ export default function Input({
"aria-label": ariaLabel, "aria-label": ariaLabel,
control,
disabled, disabled,
label,
name, name,
placeholder, placeholder = "",
registerOptions, registerOptions = {},
required = false,
type = "text", type = "text",
}: InputProps) { }: InputProps) {
const { control } = useFormContext()
const rules = {
...registerOptions,
required:
"required" in registerOptions ? !!registerOptions.required : required,
}
const { field, fieldState, formState } = useController({ const { field, fieldState, formState } = useController({
control, control,
name, name,
rules: registerOptions, rules,
}) })
return ( return (
@@ -32,18 +44,24 @@ export default function Input({
isInvalid={fieldState.invalid} isInvalid={fieldState.invalid}
isRequired={!!registerOptions?.required} isRequired={!!registerOptions?.required}
name={field.name} name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
type={type} type={type}
> >
<Body asChild fontOnly> <AriaLabel className={styles.container} htmlFor={field.name}>
<AriaInput <Body asChild fontOnly>
className={styles.input} <AriaInput
placeholder={placeholder} className={styles.input}
ref={field.ref} id={field.name}
/> name={field.name}
</Body> onBlur={field.onBlur}
<ErrorMessage errors={formState.errors} name={name} /> 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> </TextField>
) )
} }

View File

@@ -1,8 +1,44 @@
.input { .container {
border: 2px solid var(--UI-Grey-60); align-content: center;
border-radius: var(--Corner-radius-Small); background-color: var(--Main-Grey-White);
color: var(--UI-Grey-60); border-color: var(--Scandic-Beige-40);
height: 40px; 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); 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;
}

View File

@@ -1,10 +1,8 @@
import { Control, RegisterOptions } from "react-hook-form" import type { RegisterOptions } from "react-hook-form"
import { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { extends React.InputHTMLAttributes<HTMLInputElement> {
control: Control<EditProfileSchema> label: string
name: keyof EditProfileSchema name: string
registerOptions?: RegisterOptions<EditProfileSchema> registerOptions?: RegisterOptions
} }

View 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>
)
}

View 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;
}

View 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
}

View 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",
},
})

View File

@@ -1,99 +1,137 @@
"use client" "use client"
import "react-international-phone/style.css" 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 { useController, useFormContext, useWatch } from "react-hook-form"
import { import {
defaultCountries, CountrySelector,
getCountry, DialCodePreview,
PhoneInput, ParsedCountry,
type PhoneInputRefType, usePhoneInput,
} from "react-international-phone" } from "react-international-phone"
import { useIntl } from "react-intl"
import { ChevronDownIcon } from "@/components/Icons"
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage" 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 styles from "./phone.module.css"
import type { ChangeEvent } from "react"
import type { PhoneProps } from "./phone" import type { PhoneProps } from "./phone"
export default function Phone({ export default function Phone({
countrySelectName = "country", ariaLabel = "Phone number input",
disabled = false,
label,
name = "phoneNumber", name = "phoneNumber",
placeholder = "", placeholder = "",
registerOptions = { registerOptions = {
required: true, required: true,
}, },
}: PhoneProps) { }: PhoneProps) {
const phoneRef = useRef<PhoneInputRefType>(null) const { formatMessage } = useIntl()
const { control, formState } = useFormContext() const { control, setValue } = useFormContext()
const countryValue = useWatch({ name: countrySelectName }) const phone = useWatch({ name })
const defaultCountry = getCountry({
countries: defaultCountries, const { field, fieldState, formState } = useController({
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({
control, control,
disabled,
name, name,
rules: registerOptions, rules: registerOptions,
}) })
const handleCountrySelectForPhone = useCallback((country: string) => { const { country, handlePhoneValueChange, inputValue, setCountry } =
const selectedCountry = getCountry({ usePhoneInput({
countries: defaultCountries, defaultCountry:
field: "iso2", parsePhoneNumber(
value: country.toLowerCase(), formState.defaultValues?.phoneNumber
).country?.toLowerCase() ?? "sv",
disableCountryGuess: true,
forceDialCode: true,
value: phone,
}) })
if (selectedCountry) { function handleSelectCountry(value: ParsedCountry) {
phoneRef.current?.setCountry(selectedCountry.iso2) setCountry(value.iso2)
prevSelectedCountry.current = country.toLowerCase() }
}
}, [])
useEffect(() => { function handleChange(evt: ChangeEvent<HTMLInputElement>) {
if (countryValue) { handlePhoneValueChange(evt)
if (field.value) { setValue(name, evt.target.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])
return ( return (
<div className={styles.phone}> <div className={styles.phone}>
<PhoneInput <CountrySelector
{...field} dropdownArrowClassName={styles.arrow}
className={styles.input} flagClassName={styles.flag}
defaultCountry={defaultCountry?.iso2 ?? "se"} onSelect={handleSelectCountry}
placeholder={placeholder}
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]} 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} /> <ErrorMessage errors={formState.errors} name={name} />
</div> </div>
) )

View File

@@ -1,26 +1,130 @@
.phone { .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; display: grid;
/* r-i-p sets their width dynamically and doesn't respect the width property of its parent */ gap: var(--Spacing-x2);
grid-template-columns: 470px minmax(203px, 1fr); grid-template-columns: max(164px) 1fr;
width: min(280px, 100%);
--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:has(.input:active, .input:focus) {
.phone :global(.react-international-phone-country-selector-button), --react-international-phone-border-color: var(--Scandic-Blue-90);
.phone :global(.react-international-phone-input) {
border-width: 2px;
} }
.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); 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;
} }

View File

@@ -1,7 +1,9 @@
import type { RegisterOptions } from "react-hook-form" import type { RegisterOptions } from "react-hook-form"
export type PhoneProps = { export type PhoneProps = {
countrySelectName?: string ariaLabel?: string
disabled?: boolean
label: string
name?: string name?: string
placeholder?: string placeholder?: string
registerOptions?: RegisterOptions registerOptions?: RegisterOptions

View File

@@ -1,94 +1,35 @@
"use client" "use client"
import { useState } from "react" import { useController, useFormContext } from "react-hook-form"
import {
Button,
type Key,
Label,
ListBox,
ListBoxItem,
Popover,
Select as ReactAriaSelect,
SelectValue,
} from "react-aria-components"
import Body from "../../Text/Body" import ReactAriaSelect from "@/components/TempDesignSystem/Select"
import Footnote from "../../Text/Footnote"
import SelectChevron from "../SelectChevron"
import styles from "./select.module.css" import type { SelectProps } from "./select"
import type { SelectPortalContainer, SelectProps } from "./select"
export default function Select({ export default function Select({
"aria-label": ariaLabel,
items, items,
label, label,
onSelect, name,
placeholder, placeholder,
value, registerOptions = {},
defaultSelectedKey,
}: SelectProps) { }: SelectProps) {
const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(null) const { control } = useFormContext()
const { field } = useController({
function setRef(node: SelectPortalContainer) { control,
if (node) { name,
setRootDiv(node) rules: registerOptions,
} })
}
function handleOnSelect(key: Key) {
onSelect(key)
}
return ( return (
<div className={styles.container} ref={setRef}> <ReactAriaSelect
<ReactAriaSelect defaultSelectedKey={field.value}
defaultSelectedKey={defaultSelectedKey} disabled={field.disabled}
aria-label={ariaLabel} items={items}
className={styles.select} label={label}
onSelectionChange={handleOnSelect} name={field.name}
placeholder={placeholder} onBlur={field.onBlur}
selectedKey={value as Key} onSelect={field.onChange}
> placeholder={placeholder}
<Body asChild fontOnly> value={field.value}
<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>
) )
} }

View File

@@ -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 export interface SelectProps
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> { extends Omit<
items: { label: string; value: Key }[] React.SelectHTMLAttributes<HTMLSelectElement>,
label: string "name" | "onSelect"
name: string >,
onSelect: (key: Key) => void Omit<ReactAriaSelectProps, "onSelect" | "ref" | "value"> {
placeholder?: string registerOptions?: RegisterOptions
defaultSelectedKey?: Key
} }
export type SelectPortalContainer = HTMLDivElement | null

View File

@@ -5,7 +5,7 @@ import styles from "./chevron.module.css"
export default function SelectChevron() { export default function SelectChevron() {
return ( return (
<span aria-hidden="true" className={styles.chevron}> <span aria-hidden="true" className={styles.chevron}>
<ChevronDownIcon height={24} width={24} /> <ChevronDownIcon color="grey80" />
</span> </span>
) )
} }

View File

@@ -56,7 +56,7 @@
} }
.activeSidebar { .activeSidebar {
background-color: var(--Scandic-Brand-Warm-White); background-color: var(--Scandic-Brand-Pale-Peach);
} }
.black { .black {

View File

@@ -10,6 +10,7 @@ export const linkVariants = cva(styles.link, {
color: { color: {
black: styles.black, black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
none: "",
pale: styles.pale, pale: styles.pale,
peach80: styles.peach80, peach80: styles.peach80,
}, },

View 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>
)
}

View File

@@ -2,10 +2,6 @@
position: relative; position: relative;
} }
.label {
color: var(--Base-Text-UI-Placeholder);
}
.select { .select {
border: 1px solid var(--Base-Border-Normal); border: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-radius-Medium); border-radius: var(--Corner-radius-Medium);
@@ -27,7 +23,7 @@
color: var(--Base-Text-UI-High-contrast); color: var(--Base-Text-UI-High-contrast);
display: flex; display: flex;
gap: var(--Spacing-x-half); gap: var(--Spacing-x-half);
height: 56px; height: 60px;
outline: none; outline: none;
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
text-align: left; text-align: left;
@@ -69,4 +65,4 @@
.listBoxItem[data-selected="true"] { .listBoxItem[data-selected="true"] {
font-weight: 500; font-weight: 500;
} }

View 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

View File

@@ -5,18 +5,22 @@
} }
.one { .one {
font-size: clamp(var(--typography-Script-1-Mobile-fontSize), font-size: clamp(
1.3vw + 14px, var(--typography-Script-1-Mobile-fontSize),
var(--typography-Script-1-Desktop-fontSize)); 1.3vw + 14px,
var(--typography-Script-1-Desktop-fontSize)
);
font-weight: var(--typography-Script-1-fontWeight); font-weight: var(--typography-Script-1-fontWeight);
letter-spacing: var(--typography-Script-1-letterSpacing); letter-spacing: var(--typography-Script-1-letterSpacing);
line-height: var(--typography-Script-1-lineHeight); line-height: var(--typography-Script-1-lineHeight);
} }
.two { .two {
font-size: clamp(var(--typography-Script-2-Mobile-fontSize), font-size: clamp(
0.6vw + 15px, var(--typography-Script-2-Mobile-fontSize),
var(--typography-Script-2-Desktop-fontSize)); 0.6vw + 15px,
var(--typography-Script-2-Desktop-fontSize)
);
font-weight: var(--typography-Script-2-fontWeight); font-weight: var(--typography-Script-2-fontWeight);
letter-spacing: var(--typography-Script-2-letterSpacing); letter-spacing: var(--typography-Script-2-letterSpacing);
line-height: var(--typography-Script-2-lineHeight); line-height: var(--typography-Script-2-lineHeight);
@@ -31,7 +35,7 @@
} }
.black { .black {
color: #000; color: var(--Main-Grey-100);
} }
.burgundy { .burgundy {
@@ -44,4 +48,4 @@
.plosa { .plosa {
color: var(--Theme-Primary-Light-On-Surface-Accent); color: var(--Theme-Primary-Light-On-Surface-Accent);
} }

View File

@@ -43,8 +43,7 @@
} }
.black { .black {
/* No black variable exist yet */ color: var(--Main-Grey-100);
color: #000;
} }
.burgundy { .burgundy {

View File

@@ -26,8 +26,7 @@
} }
.black { .black {
/* No black variable exist yet */ color: var(--Main-Grey-100);
color: #000;
} }
.burgundy { .burgundy {

View File

@@ -34,8 +34,7 @@
} }
.black { .black {
/* No black variable exist yet */ color: var(--Main-Grey-100);
color: #000;
} }
.burgundy { .burgundy {

View File

@@ -28,6 +28,10 @@
text-align: left; text-align: left;
} }
.black {
color: var(--Main-Grey-100);
}
.burgundy { .burgundy {
color: var(--Scandic-Brand-Burgundy); color: var(--Scandic-Brand-Burgundy);
} }

View File

@@ -5,6 +5,7 @@ import styles from "./subtitle.module.css"
const config = { const config = {
variants: { variants: {
color: { color: {
black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
pale: styles.pale, pale: styles.pale,
}, },

View File

@@ -85,7 +85,7 @@
} }
.black { .black {
color: #000; color: var(--Main-Grey-100);
} }
.burgundy { .burgundy {
@@ -96,6 +96,10 @@
color: var(--Scandic-Brand-Pale-Peach); color: var(--Scandic-Brand-Pale-Peach);
} }
.peach80 {
color: var(--Scandic-Peach-80);
}
.red { .red {
color: var(--Scandic-Brand-Scandic-Red); color: var(--Scandic-Brand-Scandic-Red);
} }

View File

@@ -8,6 +8,7 @@ const config = {
black: styles.black, black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
pale: styles.pale, pale: styles.pale,
peach80: styles.peach80,
red: styles.red, red: styles.red,
}, },
textAlign: { textAlign: {

View File

@@ -18,17 +18,19 @@
"Continue": "Blive ved", "Continue": "Blive ved",
"Could not find requested resource": "Kunne ikke finde den anmodede ressource", "Could not find requested resource": "Kunne ikke finde den anmodede ressource",
"Country": "Land", "Country": "Land",
"Country code": "Landekode",
"Current level": "Nuværende niveau", "Current level": "Nuværende niveau",
"Current password": "Nuværende kodeord",
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
"Day": "Dag", "Day": "Dag",
"Description": "Beskrivelse", "Description": "Beskrivelse",
"Discard changes": "Kassér ændringer",
"Edit": "Redigere", "Edit": "Redigere",
"Edit profile": "Rediger profil", "Edit profile": "Rediger profil",
"Email": "E-mail", "Email": "E-mail",
"Empty": "Empty", "Empty": "Empty",
"Explore all levels and benefits": "Udforsk alle niveauer og fordele", "Explore all levels and benefits": "Udforsk alle niveauer og fordele",
"Find booking": "Find booking", "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", "Get inspired": "Blive inspireret",
"Go back to overview": "Gå tilbage til oversigten", "Go back to overview": "Gå tilbage til oversigten",
"How it works": "Hvordan det virker", "How it works": "Hvordan det virker",
@@ -45,6 +47,7 @@
"My credit cards": "Mine kreditkort", "My credit cards": "Mine kreditkort",
"My pages": "Mine sider", "My pages": "Mine sider",
"My wishes": "Mine ønsker", "My wishes": "Mine ønsker",
"New password": "Nyt kodeord",
"Next": "Næste", "Next": "Næste",
"Next level": "Næste niveau", "Next level": "Næste niveau",
"No content published": "Intet indhold offentliggjort", "No content published": "Intet indhold offentliggjort",
@@ -66,10 +69,12 @@
"Previous victories": "Tidligere sejre", "Previous victories": "Tidligere sejre",
"points until next level": "point indtil næste niveau", "points until next level": "point indtil næste niveau",
"Read more": "Læs mere", "Read more": "Læs mere",
"Retype new password": "Gentag den nye adgangskode",
"Save": "Gemme", "Save": "Gemme",
"Select a country": "Vælg et land", "Select a country": "Vælg et land",
"Select country of residence": "Vælg bopælsland", "Select country of residence": "Vælg bopælsland",
"Select date of birth": "Vælg fødselsdato", "Select date of birth": "Vælg fødselsdato",
"Select language": "Vælg sprog",
"Show more": "Vis mere", "Show more": "Vis mere",
"Skip to main content": "Spring over og gå til hovedindhold", "Skip to main content": "Spring over og gå til hovedindhold",
"Something went wrong!": "Noget gik galt!", "Something went wrong!": "Noget gik galt!",
@@ -77,6 +82,7 @@
"Total Points": "Samlet antal point", "Total Points": "Samlet antal point",
"Transaction date": "Overførselsdato", "Transaction date": "Overførselsdato",
"Transactions": "Transaktioner", "Transactions": "Transaktioner",
"User information": "Brugeroplysninger",
"Visiting address": "Besøgsadresse", "Visiting address": "Besøgsadresse",
"Where should you go next?": "Hvor skal du tage hen næste gang?", "Where should you go next?": "Hvor skal du tage hen næste gang?",
"Year": "År", "Year": "År",

View File

@@ -18,17 +18,19 @@
"Continue": "Weitermachen", "Continue": "Weitermachen",
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.", "Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
"Country": "Land", "Country": "Land",
"Country code": "Landesvorwahl",
"Current level": "Aktuelles Level", "Current level": "Aktuelles Level",
"Current password": "Aktuelles Passwort",
"Date of Birth": "Geburtsdatum", "Date of Birth": "Geburtsdatum",
"Day": "Tag", "Day": "Tag",
"Description": "Beschreibung", "Description": "Beschreibung",
"Discard changes": "Änderungen verwerfen",
"Edit": "Bearbeiten", "Edit": "Bearbeiten",
"Edit profile": "Profil bearbeiten", "Edit profile": "Profil bearbeiten",
"Email": "Email", "Email": "Email",
"Empty": "Empty", "Empty": "Empty",
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile", "Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
"Find booking": "Buchung finden", "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", "Get inspired": "Lass dich inspirieren",
"Go back to overview": "Zurück zur Übersicht", "Go back to overview": "Zurück zur Übersicht",
"How it works": "Wie es funktioniert", "How it works": "Wie es funktioniert",
@@ -45,6 +47,7 @@
"My credit cards": "Meine Kreditkarten", "My credit cards": "Meine Kreditkarten",
"My pages": "Meine Seiten", "My pages": "Meine Seiten",
"My wishes": "Meine Wünsche", "My wishes": "Meine Wünsche",
"New password": "Neues Kennwort",
"Next": "Nächste", "Next": "Nächste",
"Next level": "Nächste Ebene", "Next level": "Nächste Ebene",
"No content published": "Kein Inhalt veröffentlicht", "No content published": "Kein Inhalt veröffentlicht",
@@ -66,10 +69,12 @@
"Previous victories": "Bisherige Siege", "Previous victories": "Bisherige Siege",
"points until next level": "punkte bis zum nächsten Level", "points until next level": "punkte bis zum nächsten Level",
"Read more": "Mehr lesen", "Read more": "Mehr lesen",
"Retype new password": "Neues Passwort erneut eingeben",
"Save": "Speichern", "Save": "Speichern",
"Select a country": "Wähle ein Land", "Select a country": "Wähle ein Land",
"Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus", "Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus",
"Select date of birth": "Geburtsdatum auswählen", "Select date of birth": "Geburtsdatum auswählen",
"Select language": "Sprache auswählen",
"Show more": "Zeig mehr", "Show more": "Zeig mehr",
"Skip to main content": "Direkt zum Inhalt", "Skip to main content": "Direkt zum Inhalt",
"Something went wrong!": "Etwas ist schief gelaufen!", "Something went wrong!": "Etwas ist schief gelaufen!",
@@ -77,6 +82,7 @@
"Total Points": "Gesamtpunktzahl", "Total Points": "Gesamtpunktzahl",
"Transaction date": "Transaktionsdatum", "Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen", "Transactions": "Transaktionen",
"User information": "Nutzerinformation",
"Visiting address": "Besuchsadresse", "Visiting address": "Besuchsadresse",
"Where should you go next?": "Wohin soll es als nächstes gehen?", "Where should you go next?": "Wohin soll es als nächstes gehen?",
"Year": "Jahr", "Year": "Jahr",

View File

@@ -18,17 +18,19 @@
"Continue": "Continue", "Continue": "Continue",
"Could not find requested resource": "Could not find requested resource", "Could not find requested resource": "Could not find requested resource",
"Country": "Country", "Country": "Country",
"Country code": "Country code",
"Current level": "Current level", "Current level": "Current level",
"Current password": "Current password",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Day": "Day", "Day": "Day",
"Description": "Description", "Description": "Description",
"Discard changes": "Discard changes",
"Edit": "Edit", "Edit": "Edit",
"Edit profile": "Edit profile", "Edit profile": "Edit profile",
"Email": "Email", "Email": "Email",
"Empty": "Empty", "Empty": "Empty",
"Explore all levels and benefits": "Explore all levels and benefits", "Explore all levels and benefits": "Explore all levels and benefits",
"Find booking": "Find booking", "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", "Get inspired": "Get inspired",
"Go back to overview": "Go back to overview", "Go back to overview": "Go back to overview",
"How it works": "How it works", "How it works": "How it works",
@@ -45,6 +47,7 @@
"My credit cards": "My credit cards", "My credit cards": "My credit cards",
"My pages": "My pages", "My pages": "My pages",
"My wishes": "My wishes", "My wishes": "My wishes",
"New password": "New password",
"Next": "Next", "Next": "Next",
"Next level": "Next level", "Next level": "Next level",
"No content published": "No content published", "No content published": "No content published",
@@ -66,10 +69,12 @@
"Previous victories": "Previous victories", "Previous victories": "Previous victories",
"points until next level": "points until next level", "points until next level": "points until next level",
"Read more": "Read more", "Read more": "Read more",
"Retype new password": "Retype new password",
"Save": "Save", "Save": "Save",
"Select a country": "Select a country", "Select a country": "Select a country",
"Select country of residence": "Select country of residence", "Select country of residence": "Select country of residence",
"Select date of birth": "Select date of birth", "Select date of birth": "Select date of birth",
"Select language": "Select language",
"Show more": "Show more", "Show more": "Show more",
"Skip to main content": "Skip to main content", "Skip to main content": "Skip to main content",
"Something went wrong!": "Something went wrong!", "Something went wrong!": "Something went wrong!",
@@ -77,8 +82,9 @@
"Total Points": "Total Points", "Total Points": "Total Points",
"Transaction date": "Transaction date", "Transaction date": "Transaction date",
"Transactions": "Transactions", "Transactions": "Transactions",
"Where should you go next?": "Where should you go next?", "User information": "User information",
"Visiting address": "Visiting address", "Visiting address": "Visiting address",
"Where should you go next?": "Where should you go next?",
"Year": "Year", "Year": "Year",
"You have no previous stays.": "You have no previous stays.", "You have no previous stays.": "You have no previous stays.",
"You have no upcoming stays.": "You have no upcoming stays.", "You have no upcoming stays.": "You have no upcoming stays.",

View File

@@ -18,17 +18,19 @@
"Continue": "Jatkaa", "Continue": "Jatkaa",
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt", "Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
"Country": "Maa", "Country": "Maa",
"Country code": "Maatunnus",
"Current level": "Nykyinen taso", "Current level": "Nykyinen taso",
"Current password": "Nykyinen salasana",
"Date of Birth": "Syntymäaika", "Date of Birth": "Syntymäaika",
"Day": "Päivä", "Day": "Päivä",
"Description": "Kuvaus", "Description": "Kuvaus",
"Discard changes": "Hylkää muutokset",
"Edit": "Muokata", "Edit": "Muokata",
"Edit profile": "Muokkaa profiilia", "Edit profile": "Muokkaa profiilia",
"Email": "Sähköposti", "Email": "Sähköposti",
"Empty": "Empty", "Empty": "Empty",
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin", "Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
"Find booking": "Etsi varaus", "Find booking": "Etsi varaus",
"Free soft drink voucher for the kids when staying": "Ilmainen virvoitusjuomakuponki lapsille majoittuessaan",
"Get inspired": "Inspiroidu", "Get inspired": "Inspiroidu",
"Go back to overview": "Palaa yleiskatsaukseen", "Go back to overview": "Palaa yleiskatsaukseen",
"How it works": "Kuinka se toimii", "How it works": "Kuinka se toimii",
@@ -45,6 +47,7 @@
"My credit cards": "Minun luottokorttini", "My credit cards": "Minun luottokorttini",
"My pages": "Omat sivut", "My pages": "Omat sivut",
"My wishes": "Toiveeni", "My wishes": "Toiveeni",
"New password": "Uusi salasana",
"Next": "Seuraava", "Next": "Seuraava",
"Next level": "Seuraava taso", "Next level": "Seuraava taso",
"No content published": "Ei julkaistua sisältöä", "No content published": "Ei julkaistua sisältöä",
@@ -66,10 +69,12 @@
"Previous victories": "Edelliset voitot", "Previous victories": "Edelliset voitot",
"points until next level": "pisteitä seuraavalle tasolle", "points until next level": "pisteitä seuraavalle tasolle",
"Read more": "Lue lisää", "Read more": "Lue lisää",
"Retype new password": "Kirjoita uusi salasana uudelleen",
"Save": "Tallentaa", "Save": "Tallentaa",
"Select a country": "Valitse maa", "Select a country": "Valitse maa",
"Select country of residence": "Valitse asuinmaa", "Select country of residence": "Valitse asuinmaa",
"Select date of birth": "Valitse syntymäaika", "Select date of birth": "Valitse syntymäaika",
"Select language": "Valitse kieli",
"Show more": "Näytä lisää", "Show more": "Näytä lisää",
"Skip to main content": "Siirry pääsisältöön", "Skip to main content": "Siirry pääsisältöön",
"Something went wrong!": "Jotain meni pieleen!", "Something went wrong!": "Jotain meni pieleen!",
@@ -77,6 +82,7 @@
"Total Points": "Kokonaispisteet", "Total Points": "Kokonaispisteet",
"Transaction date": "Tapahtuman päivämäärä", "Transaction date": "Tapahtuman päivämäärä",
"Transactions": "Tapahtumat", "Transactions": "Tapahtumat",
"User information": "Käyttäjän tiedot",
"Visiting address": "Käyntiosoite", "Visiting address": "Käyntiosoite",
"Where should you go next?": "Minne sinun pitäisi mennä seuraavaksi?", "Where should you go next?": "Minne sinun pitäisi mennä seuraavaksi?",
"Year": "Vuosi", "Year": "Vuosi",

View File

@@ -18,17 +18,19 @@
"Continue": "Fortsette", "Continue": "Fortsette",
"Could not find requested resource": "Kunne ikke finne den forespurte ressursen", "Could not find requested resource": "Kunne ikke finne den forespurte ressursen",
"Country": "Land", "Country": "Land",
"Country code": "Landskode",
"Current level": "Nåværende nivå", "Current level": "Nåværende nivå",
"Current password": "Nåværende passord",
"Date of Birth": "Fødselsdato", "Date of Birth": "Fødselsdato",
"Day": "Dag", "Day": "Dag",
"Description": "Beskrivelse", "Description": "Beskrivelse",
"Discard changes": "Forkaste endringer",
"Edit": "Redigere", "Edit": "Redigere",
"Edit profile": "Rediger profil", "Edit profile": "Rediger profil",
"Email": "E-post", "Email": "E-post",
"Empty": "Empty", "Empty": "Empty",
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler", "Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
"Find booking": "Finn booking", "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", "Get inspired": "Bli inspirert",
"Go back to overview": "Gå tilbake til oversikten", "Go back to overview": "Gå tilbake til oversikten",
"How it works": "Hvordan det fungerer", "How it works": "Hvordan det fungerer",
@@ -45,6 +47,7 @@
"My credit cards": "Kredittkortene mine", "My credit cards": "Kredittkortene mine",
"My pages": "Mine sider", "My pages": "Mine sider",
"My wishes": "Mine ønsker", "My wishes": "Mine ønsker",
"New password": "Nytt passord",
"Next": "Neste", "Next": "Neste",
"Next level": "Neste nivå", "Next level": "Neste nivå",
"No content published": "Ingen innhold publisert", "No content published": "Ingen innhold publisert",
@@ -66,10 +69,12 @@
"Previous victories": "Tidligere seire", "Previous victories": "Tidligere seire",
"points until next level": "poeng til neste nivå", "points until next level": "poeng til neste nivå",
"Read more": "Les mer", "Read more": "Les mer",
"Retype new password": "Skriv inn nytt passord på nytt",
"Save": "Lagre", "Save": "Lagre",
"Select a country": "Velg et land", "Select a country": "Velg et land",
"Select country of residence": "Velg bostedsland", "Select country of residence": "Velg bostedsland",
"Select date of birth": "Velg fødselsdato", "Select date of birth": "Velg fødselsdato",
"Select language": "Velg språk",
"Show more": "Vis mer", "Show more": "Vis mer",
"Skip to main content": "Gå videre til hovedsiden", "Skip to main content": "Gå videre til hovedsiden",
"Something went wrong!": "Noe gikk galt!", "Something went wrong!": "Noe gikk galt!",
@@ -77,6 +82,7 @@
"Total Points": "Totale poeng", "Total Points": "Totale poeng",
"Transaction date": "Transaksjonsdato", "Transaction date": "Transaksjonsdato",
"Transactions": "Transaksjoner", "Transactions": "Transaksjoner",
"User information": "Brukerinformasjon",
"Visiting address": "Besøksadresse", "Visiting address": "Besøksadresse",
"Where should you go next?": "Hvor bør du gå videre?", "Where should you go next?": "Hvor bør du gå videre?",
"Year": "År", "Year": "År",

View File

@@ -18,17 +18,19 @@
"Continue": "Fortsätt", "Continue": "Fortsätt",
"Could not find requested resource": "Det gick inte att hitta den begärda resursen", "Could not find requested resource": "Det gick inte att hitta den begärda resursen",
"Country": "Land", "Country": "Land",
"Country code": "Landskod",
"Current level": "Nuvarande nivå", "Current level": "Nuvarande nivå",
"Current password": "Nuvarande lösenord",
"Date of Birth": "Födelsedatum", "Date of Birth": "Födelsedatum",
"Day": "Dag", "Day": "Dag",
"Description": "Beskrivning", "Description": "Beskrivning",
"Discard changes": "Ignorera ändringar",
"Edit": "Redigera", "Edit": "Redigera",
"Edit profile": "Redigera profil", "Edit profile": "Redigera profil",
"Email": "E-post", "Email": "E-post",
"Empty": "Tom", "Empty": "Tom",
"Explore all levels and benefits": "Utforska alla nivåer och fördelar", "Explore all levels and benefits": "Utforska alla nivåer och fördelar",
"Find booking": "Hitta bokning", "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", "Get inspired": "Bli inspirerad",
"Go back to overview": "Gå tillbaka till översikten", "Go back to overview": "Gå tillbaka till översikten",
"How it works": "Hur det fungerar", "How it works": "Hur det fungerar",
@@ -45,6 +47,7 @@
"My credit cards": "Mina kreditkort", "My credit cards": "Mina kreditkort",
"My pages": "Mina sidor", "My pages": "Mina sidor",
"My wishes": "Mina önskningar", "My wishes": "Mina önskningar",
"New password": "Nytt lösenord",
"Next": "Nästa", "Next": "Nästa",
"Next level": "Nästa nivå", "Next level": "Nästa nivå",
"No content published": "Inget innehåll publicerat", "No content published": "Inget innehåll publicerat",
@@ -66,10 +69,12 @@
"Previous victories": "Tidigare segrar", "Previous victories": "Tidigare segrar",
"points until next level": "poäng till nästa nivå", "points until next level": "poäng till nästa nivå",
"Read more": "Läs mer", "Read more": "Läs mer",
"Retype new password": "Upprepa nytt lösenord",
"Save": "Spara", "Save": "Spara",
"Select a country": "Välj ett land", "Select a country": "Välj ett land",
"Select country of residence": "Välj bosättningsland", "Select country of residence": "Välj bosättningsland",
"Select date of birth": "Välj födelsedatum", "Select date of birth": "Välj födelsedatum",
"Select language": "Välj språk",
"Show more": "Visa mer", "Show more": "Visa mer",
"Skip to main content": "Fortsätt till huvudinnehåll", "Skip to main content": "Fortsätt till huvudinnehåll",
"Something went wrong!": "Något gick fel!", "Something went wrong!": "Något gick fel!",
@@ -77,6 +82,7 @@
"Total Points": "Total poäng", "Total Points": "Total poäng",
"Transaction date": "Transaktionsdatum", "Transaction date": "Transaktionsdatum",
"Transactions": "Transaktioner", "Transactions": "Transaktioner",
"User information": "Användar information",
"Visiting address": "Besöksadress", "Visiting address": "Besöksadress",
"Where should you go next?": "Vart ska du gå härnäst?", "Where should you go next?": "Vart ska du gå härnäst?",
"Year": "År", "Year": "År",

View File

@@ -7,6 +7,7 @@ import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countri
export const getUserSchema = z.object({ export const getUserSchema = z.object({
address: z.object({ address: z.object({
city: z.string().optional(), city: z.string().optional(),
country: z.string().optional(),
countryCode: z.nativeEnum(countriesMap).optional(), countryCode: z.nativeEnum(countriesMap).optional(),
streetAddress: z.string().optional(), streetAddress: z.string().optional(),
zipCode: z.string(), zipCode: z.string(),

View File

@@ -56,8 +56,6 @@ export const userQueryRouter = router({
} }
const apiJson = await apiResponse.json() const apiJson = await apiResponse.json()
console.log({ apiJson })
console.log({ attr: apiJson.data.attributes })
if (!apiJson.data?.attributes) { if (!apiJson.data?.attributes) {
// throw notFound(apiJson) // throw notFound(apiJson)
console.error( console.error(
@@ -78,12 +76,14 @@ export const userQueryRouter = router({
const country = countries.find( const country = countries.find(
(c) => c.code === verifiedData.data.address.countryCode (c) => c.code === verifiedData.data.address.countryCode
) )
const phonenumber = parsePhoneNumber(verifiedData.data.phoneNumber)
const user = { const user = {
...extendedUser, ...extendedUser,
address: { address: {
city: verifiedData.data.address.city, 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, streetAddress: verifiedData.data.address.streetAddress,
zipCode: verifiedData.data.address.zipCode, zipCode: verifiedData.data.address.zipCode,
}, },
@@ -94,7 +94,7 @@ export const userQueryRouter = router({
lastName: verifiedData.data.lastName, lastName: verifiedData.data.lastName,
memberships: verifiedData.data.memberships, memberships: verifiedData.data.memberships,
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`, name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
phoneNumber: verifiedData.data.phoneNumber, phoneNumber: phonenumber.formatInternational(),
profileId: verifiedData.data.profileId, profileId: verifiedData.data.profileId,
} }
@@ -111,7 +111,6 @@ export const userQueryRouter = router({
user.address.zipCode = maskValue.text(verifiedData.data.address.zipCode) user.address.zipCode = maskValue.text(verifiedData.data.address.zipCode)
user.email = maskValue.email(user.email) user.email = maskValue.email(user.email)
const phonenumber = parsePhoneNumber(user.phoneNumber)
user.phoneNumber = `+${phonenumber.countryCallingCode} ${maskValue.phone(user.phoneNumber)}` user.phoneNumber = `+${phonenumber.countryCallingCode} ${maskValue.phone(user.phoneNumber)}`
} }

View File

@@ -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 })),
}))

View File

@@ -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" import type { User } from "@/types/user"
export type EditFormProps = { export type EditFormProps = {
user: User user: User
} }
export type EditFormContentProps = {
control: Control<EditProfileSchema>
}
type E = { type E = {
message: string message: string
path: string path: string