feat(SW-160): update profile
This commit is contained in:
committed by
Michael Zetterberg
parent
b6e22d51a5
commit
2337d37f1a
@@ -10,3 +10,9 @@
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: max(164px) 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { languageSelect } from "@/constants/languages"
|
||||
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
@@ -15,24 +16,24 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import styles from "./formContent.module.css"
|
||||
|
||||
export default function FormContent() {
|
||||
const { formatMessage } = useIntl()
|
||||
const intl = 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 phoneNumber = formatMessage({ id: "Phone number" })
|
||||
const password = formatMessage({ id: "Current password" })
|
||||
const retypeNewPassword = formatMessage({ id: "Retype new password" })
|
||||
const zipCode = formatMessage({ id: "Zip code" })
|
||||
const city = intl.formatMessage({ id: "City" })
|
||||
const country = intl.formatMessage({ id: "Country" })
|
||||
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
|
||||
const street = intl.formatMessage({ id: "Address" })
|
||||
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||
const password = intl.formatMessage({ id: "Current password" })
|
||||
const retypeNewPassword = intl.formatMessage({ id: "Retype new password" })
|
||||
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={styles.user}>
|
||||
<header>
|
||||
<Body textTransform="bold">
|
||||
{formatMessage({ id: "User information" })}
|
||||
{intl.formatMessage({ id: "User information" })}
|
||||
</Body>
|
||||
</header>
|
||||
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
|
||||
@@ -59,16 +60,19 @@ export default function FormContent() {
|
||||
<Phone label={phoneNumber} name="phoneNumber" />
|
||||
<Select
|
||||
items={languageSelect}
|
||||
label={formatMessage({ id: "Language" })}
|
||||
label={intl.formatMessage({ id: "Language" })}
|
||||
name="language"
|
||||
placeholder={formatMessage({ id: "Select language" })}
|
||||
placeholder={intl.formatMessage({ id: "Select language" })}
|
||||
/>
|
||||
</section>
|
||||
<Divider className={styles.divider} color="subtle" />
|
||||
<section className={styles.password}>
|
||||
<header>
|
||||
<Body textTransform="bold">{formatMessage({ id: "Password" })}</Body>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Password" })}
|
||||
</Body>
|
||||
</header>
|
||||
<Input label={password} name="currentPassword" type="password" />
|
||||
<Input label={password} name="password" type="password" />
|
||||
<NewPassword />
|
||||
<Input
|
||||
label={retypeNewPassword}
|
||||
|
||||
@@ -24,10 +24,6 @@
|
||||
grid-area: buttons;
|
||||
}
|
||||
|
||||
.btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
grid-template-areas:
|
||||
@@ -40,9 +36,9 @@
|
||||
}
|
||||
|
||||
.btnContainer {
|
||||
align-self: center;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-self: flex-end;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useFormState as useReactFormState } from "react-dom"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { usePhoneInput } from "react-international-phone"
|
||||
import { useIntl } from "react-intl"
|
||||
@@ -13,32 +13,32 @@ import { editProfile } from "@/actions/editProfile"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import FormContent from "./FormContent"
|
||||
import { type EditProfileSchema, editProfileSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
import type {
|
||||
EditFormProps,
|
||||
State,
|
||||
import {
|
||||
type EditFormProps,
|
||||
Status,
|
||||
} from "@/types/components/myPages/myProfile/edit"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const formId = "edit-profile"
|
||||
|
||||
export default function Form({ user }: EditFormProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const lang = params.lang as Lang
|
||||
/**
|
||||
* like react, react-hook-form also exports a useFormState hook,
|
||||
* we want to clearly keep them separate by naming.
|
||||
* RHF isValid defaults to false and never
|
||||
* changes when JS is disabled, therefore
|
||||
* we need to keep it insync ourselves
|
||||
*/
|
||||
const [state, formAction] = useReactFormState<State, FormData>(
|
||||
editProfile,
|
||||
null
|
||||
)
|
||||
const [isValid, setIsValid] = useState(true)
|
||||
|
||||
const { inputValue: phoneInput } = usePhoneInput({
|
||||
defaultCountry:
|
||||
@@ -61,7 +61,7 @@ export default function Form({ user }: EditFormProps) {
|
||||
email: user.email,
|
||||
language: user.language,
|
||||
phoneNumber: phoneInput,
|
||||
currentPassword: "",
|
||||
password: "",
|
||||
newPassword: "",
|
||||
retypeNewPassword: "",
|
||||
},
|
||||
@@ -70,44 +70,77 @@ export default function Form({ user }: EditFormProps) {
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
async function handleSubmit(data: EditProfileSchema) {
|
||||
const response = await editProfile(data)
|
||||
switch (response.status) {
|
||||
case Status.error:
|
||||
if (response.issues?.length) {
|
||||
response.issues.forEach((issue) => {
|
||||
console.error(issue)
|
||||
})
|
||||
}
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "An error occured when trying to update profile.",
|
||||
})
|
||||
)
|
||||
break
|
||||
case Status.success:
|
||||
toast.success(
|
||||
intl.formatMessage({ id: "Successfully updated profile!" })
|
||||
)
|
||||
/**
|
||||
* TODO: Toaster?
|
||||
* Design only has toasters for credit cards
|
||||
* and membership cards
|
||||
*/
|
||||
router.push(profile[lang])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid(methods.formState.isValid)
|
||||
}, [setIsValid, methods.formState.isValid])
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<hgroup className={styles.title}>
|
||||
<Title as="h4" color="red" level="h1" textTransform="capitalize">
|
||||
{formatMessage({ id: "Welcome" })}
|
||||
{intl.formatMessage({ id: "Welcome" })}
|
||||
</Title>
|
||||
<Title as="h4" color="burgundy" level="h2" textTransform="capitalize">
|
||||
{user.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<div className={styles.btnContainer}>
|
||||
<Button
|
||||
asChild
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
className={styles.btn}
|
||||
>
|
||||
<Button asChild intent="secondary" size="small" theme="base">
|
||||
<Link href={profile[lang]}>
|
||||
{formatMessage({ id: "Discard changes" })}
|
||||
{intl.formatMessage({ id: "Discard changes" })}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!methods.formState.isValid || methods.formState.isSubmitting
|
||||
}
|
||||
className={styles.btn}
|
||||
disabled={!isValid || methods.formState.isSubmitting}
|
||||
form={formId}
|
||||
intent="primary"
|
||||
size="small"
|
||||
theme="base"
|
||||
type="submit"
|
||||
>
|
||||
{formatMessage({ id: "Save" })}
|
||||
{intl.formatMessage({ id: "Save" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form action={formAction} className={styles.form} id={formId}>
|
||||
<form
|
||||
/**
|
||||
* Ignoring since ts doesn't recognize that tRPC
|
||||
* parses FormData before reaching the route
|
||||
* @ts-ignore */
|
||||
action={editProfile}
|
||||
className={styles.form}
|
||||
id={formId}
|
||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormContent />
|
||||
</FormProvider>
|
||||
|
||||
@@ -25,12 +25,12 @@ export const editProfileSchema = z
|
||||
"Please enter a valid phone number"
|
||||
),
|
||||
|
||||
currentPassword: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
newPassword: z.string().optional(),
|
||||
retypeNewPassword: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.currentPassword) {
|
||||
if (data.password) {
|
||||
if (!data.newPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
@@ -50,7 +50,7 @@ export const editProfileSchema = z
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Current password is required",
|
||||
path: ["currentPassword"],
|
||||
path: ["password"],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +58,14 @@ a.default {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* SIZES */
|
||||
@@ -798,4 +800,4 @@ a.default {
|
||||
.icon.tertiaryLightSecondary:disabled svg,
|
||||
.icon.tertiaryLightSecondary:disabled svg * {
|
||||
fill: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import type { Key } from "react-aria-components"
|
||||
|
||||
import type { DateProps } from "./date"
|
||||
|
||||
/** TODO: Get selecting with Enter-key to work */
|
||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const d = useWatch({ name })
|
||||
@@ -54,6 +53,18 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const monthLabel = formatMessage({ id: "Month" })
|
||||
const yearLabel = formatMessage({ id: "Year" })
|
||||
|
||||
let dateValue = null
|
||||
try {
|
||||
/**
|
||||
* parseDate throws when its not a valid
|
||||
* date, but we can't check isNan since
|
||||
* we recieve the date as "1999-01-01"
|
||||
*/
|
||||
dateValue = parseDate(d)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
aria-label={formatMessage({ id: "Select date of birth" })}
|
||||
@@ -61,7 +72,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
isRequired={!!registerOptions.required}
|
||||
name={name}
|
||||
ref={field.ref}
|
||||
value={isNaN(d) ? undefined : parseDate(d)}
|
||||
value={dateValue}
|
||||
>
|
||||
<Group>
|
||||
<DateInput className={styles.container}>
|
||||
|
||||
@@ -32,8 +32,8 @@ const config = {
|
||||
defaultVariants: {
|
||||
color: "burgundy",
|
||||
textAlign: "left",
|
||||
type: "h1",
|
||||
textTransform: "uppercase",
|
||||
type: "h1",
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
Reference in New Issue
Block a user