diff --git a/actions/editProfile.ts b/actions/editProfile.ts index b1d10ea60..39dde0cc5 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -1,25 +1,41 @@ "use server" - -// import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" import { ZodError } from "zod" +import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" + import { type State, Status } from "@/types/components/myPages/myProfile/edit" export async function editProfile(_prevState: State, values: FormData) { try { - const data = Object.fromEntries(values.entries()) + const data: Record = Object.fromEntries(values.entries()) /** * TODO: Update profile data when endpoint from * API team is ready */ - console.info(`EditProfileSchema.Parse Data`) + console.info(`Raw Data`) console.log(data) - - return { - message: "All good!", - status: Status.success, + data.address = { + city: data["address.city"], + countryCode: data["address.countryCode"], + streetAddress: data["address.streetAddress"], + zipCode: data["address.zipCode"], + } + const parsedData = editProfileSchema.safeParse(data) + if (parsedData.success) { + console.info(`Success`) + console.log(parsedData.data) + return { + message: "All good!", + status: Status.success, + } + } else { + console.error(parsedData.error) + return { + message: "Invalid data, parse failed!", + status: Status.error, + } } } catch (error) { if (error instanceof ZodError) { diff --git a/components/Forms/Edit/Profile/FormContent/index.tsx b/components/Forms/Edit/Profile/FormContent/index.tsx index db83a0c22..c0f167f41 100644 --- a/components/Forms/Edit/Profile/FormContent/index.tsx +++ b/components/Forms/Edit/Profile/FormContent/index.tsx @@ -7,6 +7,7 @@ import { languageSelect } from "@/constants/languages" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import DateSelect from "@/components/TempDesignSystem/Form/Date" import Input from "@/components/TempDesignSystem/Form/Input" +import NewPassword from "@/components/TempDesignSystem/Form/NewPassword" import Phone from "@/components/TempDesignSystem/Form/Phone" import Select from "@/components/TempDesignSystem/Form/Select" import Body from "@/components/TempDesignSystem/Text/Body" @@ -21,8 +22,8 @@ export default function FormContent() { const country = formatMessage({ id: "Country" }) const email = `${formatMessage({ id: "Email" })} ${formatMessage({ id: "Address" }).toLowerCase()}` const street = formatMessage({ id: "Address" }) + const phoneNumber = formatMessage({ id: "Phone number" }) const password = formatMessage({ id: "Current password" }) - const newPassword = formatMessage({ id: "New password" }) const retypeNewPassword = formatMessage({ id: "Retype new password" }) const zipCode = formatMessage({ id: "Zip code" }) @@ -46,7 +47,7 @@ export default function FormContent() { label={zipCode} name="address.zipCode" placeholder={zipCode} - required + registerOptions={{ required: true }} /> - - + + ({ + const methods = useForm({ defaultValues: { - "address.city": user.address.city ?? "", - "address.countryCode": user.address.countryCode ?? "", - "address.streetAddress": user.address.streetAddress ?? "", - "address.zipCode": user.address.zipCode ?? "", + address: { + city: user.address.city ?? "", + countryCode: user.address.countryCode ?? "", + streetAddress: user.address.streetAddress ?? "", + zipCode: user.address.zipCode ?? "", + }, dateOfBirth: user.dateOfBirth, email: user.email, language: user.language, @@ -53,7 +61,7 @@ export default function Form({ user }: EditFormProps) { }) return ( - + <>
@@ -64,17 +72,15 @@ export default function Form({ user }: EditFormProps) {
-
-
- + + + -
+ ) } diff --git a/components/Forms/Edit/Profile/schema.ts b/components/Forms/Edit/Profile/schema.ts index 6c5fe9e2b..1460c78ac 100644 --- a/components/Forms/Edit/Profile/schema.ts +++ b/components/Forms/Edit/Profile/schema.ts @@ -1,24 +1,98 @@ import { z } from "zod" -// import { phoneValidator } from "@/utils/phoneValidator" +import { Key } from "@/components/TempDesignSystem/Form/NewPassword/newPassword" +import { phoneValidator } from "@/utils/phoneValidator" -export const editProfileSchema = z.object({ - "address.city": z.string().optional(), - "address.countryCode": z.string().min(1), - "address.streetAddress": z.string().optional(), - "address.zipCode": z.string().min(1), - dateOfBirth: z.string().min(1), - email: z.string().email(), - language: z.string(), - phoneNumber: z.string(), - // phoneValidator( - // "Phone is required", - // "Please enter a valid phone number" - // ), +const countryRequiredMsg = "Country is required" +export const editProfileSchema = z + .object({ + address: z.object({ + city: z.string().optional(), + countryCode: z + .string({ + required_error: countryRequiredMsg, + invalid_type_error: countryRequiredMsg, + }) + .min(1, countryRequiredMsg), + streetAddress: z.string().optional(), + zipCode: z.string().min(1, "Zip code is required"), + }), + dateOfBirth: z.string().min(1), + email: z.string().email(), + language: z.string(), + phoneNumber: phoneValidator( + "Phone is required", + "Please enter a valid phone number" + ), - currentPassword: z.string().optional(), - newPassword: z.string().optional(), - retypeNewPassword: z.string().optional(), -}) + currentPassword: z.string().optional(), + newPassword: z.string().optional(), + retypeNewPassword: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.currentPassword) { + if (!data.newPassword) { + ctx.addIssue({ + code: "custom", + message: "New password is required", + path: ["newPassword"], + }) + } + if (!data.retypeNewPassword) { + ctx.addIssue({ + code: "custom", + message: "Retype new password is required", + path: ["retypeNewPassword"], + }) + } + } else { + if (data.newPassword || data.retypeNewPassword) { + ctx.addIssue({ + code: "custom", + message: "Current password is required", + path: ["currentPassword"], + }) + } + } + + if (data.newPassword) { + const msgs = [] + if (data.newPassword.length < 10 || data.newPassword.length > 40) { + msgs.push(Key.CHAR_LENGTH) + } + if (!data.newPassword.match(/[A-Z]/g)) { + msgs.push(Key.UPPERCASE) + } + if (!data.newPassword.match(/[0-9]/g)) { + msgs.push(Key.NUM) + } + if (!data.newPassword.match(/[^A-Za-z0-9]/g)) { + msgs.push(Key.SPECIAL_CHAR) + } + if (msgs.length) { + ctx.addIssue({ + code: "custom", + message: msgs.join(","), + path: ["newPassword"], + }) + } + } + + if (data.newPassword && !data.retypeNewPassword) { + ctx.addIssue({ + code: "custom", + message: "Retype new password is required", + path: ["retypeNewPassword"], + }) + } + + if (data.retypeNewPassword !== data.newPassword) { + ctx.addIssue({ + code: "custom", + message: "Retype new password does not match new password", + path: ["retypeNewPassword"], + }) + } + }) export type EditProfileSchema = z.infer diff --git a/components/Icons/Close.tsx b/components/Icons/Close.tsx new file mode 100644 index 000000000..6fdb13a67 --- /dev/null +++ b/components/Icons/Close.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function CloseIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 4907399f8..7db0306cf 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -6,6 +6,7 @@ export { default as CheckIcon } from "./Check" export { default as CheckCircleIcon } from "./CheckCircle" export { default as ChevronDownIcon } from "./ChevronDown" export { default as ChevronRightIcon } from "./ChevronRight" +export { default as CloseIcon } from "./Close" export { default as EmailIcon } from "./Email" export { default as GlobeIcon } from "./Globe" export { default as HouseIcon } from "./House" diff --git a/components/TempDesignSystem/Form/Country/country.module.css b/components/TempDesignSystem/Form/Country/country.module.css index 082c648ba..1d0fbacb5 100644 --- a/components/TempDesignSystem/Form/Country/country.module.css +++ b/components/TempDesignSystem/Form/Country/country.module.css @@ -19,6 +19,13 @@ padding: var(--Spacing-x1) var(--Spacing-x2); } +.comboBoxContainer:has( + .input[data-invalid="true"], + .input[aria-invalid="true"] + ) { + border-color: var(--Scandic-Red-60); +} + .label { grid-area: label; } @@ -66,6 +73,8 @@ var(--Spacing-x2); } +.listBoxItem[data-focused="true"], +.listBoxItem[data-focus-visible="true"], .listBoxItem[data-selected="true"], .listBoxItem:hover { background-color: var(--Scandic-Blue-00); diff --git a/components/TempDesignSystem/Form/Country/index.tsx b/components/TempDesignSystem/Form/Country/index.tsx index 8a6bae687..3ff4d8c72 100644 --- a/components/TempDesignSystem/Form/Country/index.tsx +++ b/components/TempDesignSystem/Form/Country/index.tsx @@ -1,10 +1,8 @@ "use client" -import { ErrorMessage } from "@hookform/error-message" import { useState } from "react" import { Button, ComboBox, - FieldError, Input, type Key, ListBox, @@ -18,6 +16,7 @@ import Label from "@/components/TempDesignSystem/Form/Label" import SelectChevron from "@/components/TempDesignSystem/Form/SelectChevron" import Body from "@/components/TempDesignSystem/Text/Body" +import ErrorMessage from "../ErrorMessage" import { countries } from "./countries" import styles from "./country.module.css" @@ -42,7 +41,7 @@ export default function CountrySelect({ } } const { control, setValue } = useFormContext() - const { field } = useController({ + const { field, fieldState, formState } = useController({ control, name, rules: registerOptions, @@ -60,6 +59,7 @@ export default function CountrySelect({ aria-label={formatMessage({ id: "Select country of residence" })} className={styles.select} isRequired={!!registerOptions?.required} + isInvalid={fieldState.invalid} name={field.name} onBlur={field.onBlur} onSelectionChange={handleChange} @@ -85,9 +85,7 @@ export default function CountrySelect({ - - - + { name: string - registerOptions: RegisterOptions + registerOptions?: RegisterOptions } diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 289892417..6596f88e3 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -1,11 +1,6 @@ "use client" import { parseDate } from "@internationalized/date" -import { - DateInput, - DatePicker, - DateSegment, - Group, -} from "react-aria-components" +import { DateInput, DatePicker, Group } from "react-aria-components" import { useController, useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" @@ -23,7 +18,7 @@ import type { Key } from "react-aria-components" import type { DateProps } from "./date" /** TODO: Get selecting with Enter-key to work */ -export default function DateSelect({ name, registerOptions }: DateProps) { +export default function DateSelect({ name, registerOptions = {} }: DateProps) { const { formatMessage } = useIntl() const d = useWatch({ name }) const { control, setValue } = useFormContext() @@ -84,7 +79,7 @@ export default function DateSelect({ name, registerOptions }: DateProps) { ) } return ( - +
- +
) case "year": return ( - +