diff --git a/apps/scandic-web/components/Forms/Edit/Profile/schema.ts b/apps/scandic-web/components/Forms/Edit/Profile/schema.ts index 8aad6c1fd..249c0e9bc 100644 --- a/apps/scandic-web/components/Forms/Edit/Profile/schema.ts +++ b/apps/scandic-web/components/Forms/Edit/Profile/schema.ts @@ -3,26 +3,36 @@ import { z } from "zod" import { passwordValidator } from "@/utils/zod/passwordValidator" import { phoneValidator } from "@/utils/zod/phoneValidator" -const countryRequiredMsg = "Country is required" +export const editProfileErrors = { + COUNTRY_REQUIRED: "COUNTRY_REQUIRED", + ZIP_CODE_REQUIRED: "ZIP_CODE_REQUIRED", + PHONE_REQUIRED: "PHONE_REQUIRED", + PHONE_REQUESTED: "PHONE_REQUESTED", + PASSWORD_NEW_REQUIRED: "PASSWORD_NEW_REQUIRED", + PASSWORD_RETYPE_NEW_REQUIRED: "PASSWORD_RETYPE_NEW_REQUIRED", + PASSWORD_CURRENT_REQUIRED: "PASSWORD_CURRENT_REQUIRED", + PASSWORD_NEW_NOT_MATCH: "PASSWORD_NEW_NOT_MATCH", +} as const + export const editProfileSchema = z .object({ address: z.object({ city: z.string().optional(), countryCode: z .string({ - required_error: countryRequiredMsg, - invalid_type_error: countryRequiredMsg, + required_error: editProfileErrors.COUNTRY_REQUIRED, + invalid_type_error: editProfileErrors.COUNTRY_REQUIRED, }) - .min(1, countryRequiredMsg), + .min(1, editProfileErrors.COUNTRY_REQUIRED), streetAddress: z.string().optional(), - zipCode: z.string().min(1, "Zip code is required"), + zipCode: z.string().min(1, editProfileErrors.ZIP_CODE_REQUIRED), }), dateOfBirth: z.string().min(1), email: z.string().email(), language: z.string(), phoneNumber: phoneValidator( - "Phone is required", - "Please enter a valid phone number" + editProfileErrors.PHONE_REQUIRED, + editProfileErrors.PHONE_REQUESTED ), password: z.string().optional(), @@ -34,14 +44,14 @@ export const editProfileSchema = z if (!data.newPassword) { ctx.addIssue({ code: "custom", - message: "New password is required", + message: editProfileErrors.PASSWORD_NEW_REQUIRED, path: ["newPassword"], }) } if (!data.retypeNewPassword) { ctx.addIssue({ code: "custom", - message: "Retype new password is required", + message: editProfileErrors.PASSWORD_RETYPE_NEW_REQUIRED, path: ["retypeNewPassword"], }) } @@ -49,7 +59,7 @@ export const editProfileSchema = z if (data.newPassword || data.retypeNewPassword) { ctx.addIssue({ code: "custom", - message: "Current password is required", + message: editProfileErrors.PASSWORD_CURRENT_REQUIRED, path: ["password"], }) } @@ -58,7 +68,7 @@ export const editProfileSchema = z if (data.newPassword && !data.retypeNewPassword) { ctx.addIssue({ code: "custom", - message: "Retype new password is required", + message: editProfileErrors.PASSWORD_RETYPE_NEW_REQUIRED, path: ["retypeNewPassword"], }) } @@ -66,7 +76,7 @@ export const editProfileSchema = z if (data.retypeNewPassword !== data.newPassword) { ctx.addIssue({ code: "custom", - message: "Retype new password does not match new password", + message: editProfileErrors.PASSWORD_NEW_NOT_MATCH, path: ["retypeNewPassword"], }) } diff --git a/apps/scandic-web/components/Forms/Signup/schema.ts b/apps/scandic-web/components/Forms/Signup/schema.ts index 37969893e..71b7e56a7 100644 --- a/apps/scandic-web/components/Forms/Signup/schema.ts +++ b/apps/scandic-web/components/Forms/Signup/schema.ts @@ -3,35 +3,45 @@ import { z } from "zod" import { passwordValidator } from "@/utils/zod/passwordValidator" import { phoneValidator } from "@/utils/zod/phoneValidator" -const countryRequiredMsg = "Country is required" +export const signupErrors = { + COUNTRY_REQUIRED: "COUNTRY_REQUIRED", + FIRST_NAME_REQUIRED: "FIRST_NAME_REQUIRED", + LAST_NAME_REQUIRED: "LAST_NAME_REQUIRED", + PHONE_REQUIRED: "PHONE_REQUIRED", + PHONE_REQUESTED: "PHONE_REQUESTED", + BIRTH_DATE_REQUIRED: "BIRTH_DATE_REQUIRED", + PASSWORD_REQUIRED: "PASSWORD_REQUIRED", + TERMS_REQUIRED: "TERMS_REQUIRED", +} as const + export const signUpSchema = z.object({ - firstName: z.string().max(250).trim().min(1, { - message: "First name is required", - }), - lastName: z.string().max(250).trim().min(1, { - message: "Last name is required", - }), + firstName: z + .string() + .max(250) + .trim() + .min(1, signupErrors.FIRST_NAME_REQUIRED), + lastName: z.string().max(250).trim().min(1, signupErrors.LAST_NAME_REQUIRED), email: z.string().max(250).email(), phoneNumber: phoneValidator( - "Phone is required", - "Please enter a valid phone number" + signupErrors.PHONE_REQUIRED, + signupErrors.PHONE_REQUESTED ), dateOfBirth: z.string().min(1, { - message: "Date of birth is required", + message: signupErrors.BIRTH_DATE_REQUIRED, }), address: z.object({ countryCode: z .string({ - required_error: countryRequiredMsg, - invalid_type_error: countryRequiredMsg, + required_error: signupErrors.COUNTRY_REQUIRED, + invalid_type_error: signupErrors.COUNTRY_REQUIRED, }) - .min(1, countryRequiredMsg), + .min(1, signupErrors.COUNTRY_REQUIRED), zipCode: z.string().min(1), }), - password: passwordValidator("Password is required"), - termsAccepted: z.boolean().refine((value) => value === true, { - message: "You must accept the terms and conditions", - }), + password: passwordValidator(signupErrors.PASSWORD_REQUIRED), + termsAccepted: z + .boolean() + .refine((value) => value === true, signupErrors.TERMS_REQUIRED), }) export type SignUpSchema = z.infer diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts index 07f63d86b..e5cdae938 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/Multiroom/schema.ts @@ -9,23 +9,35 @@ const stringMatcher = const isValidString = (key: string) => stringMatcher.test(key) +export const multiroomErrors = { + COUNTRY_REQUIRED: "COUNTRY_REQUIRED", + FIRST_NAME_REQUIRED: "FIRST_NAME_REQUIRED", + FIRST_NAME_SPECIAL_CHARACTERS: "FIRST_NAME_SPECIAL_CHARACTERS", + LAST_NAME_REQUIRED: "LAST_NAME_REQUIRED", + LAST_NAME_SPECIAL_CHARACTERS: "LAST_NAME_SPECIAL_CHARACTERS", + PHONE_REQUIRED: "PHONE_REQUIRED", + PHONE_REQUESTED: "PHONE_REQUESTED", + EMAIL_REQUIRED: "EMAIL_REQUIRED", + MEMBERSHIP_NO_ONLY_DIGITS: "MEMBERSHIP_NO_ONLY_DIGITS", + MEMBERSHIP_NO_INVALID: "MEMBERSHIP_NO_INVALID", +} as const + export const multiroomDetailsSchema = z.object({ - countryCode: z.string().min(1, { message: "Country is required" }), - email: z.string().email({ message: "Email address is required" }), + countryCode: z.string().min(1, multiroomErrors.COUNTRY_REQUIRED), + email: z.string().email(multiroomErrors.EMAIL_REQUIRED), firstName: z .string() - .min(1, { message: "First name is required" }) - .refine(isValidString, { - message: "First name can't contain any special characters", - }), + .min(1, multiroomErrors.FIRST_NAME_REQUIRED) + .refine(isValidString, multiroomErrors.FIRST_NAME_SPECIAL_CHARACTERS), join: z.boolean().default(false), lastName: z .string() - .min(1, { message: "Last name is required" }) - .refine(isValidString, { - message: "Last name can't contain any special characters", - }), - phoneNumber: phoneValidator(), + .min(1, multiroomErrors.LAST_NAME_REQUIRED) + .refine(isValidString, multiroomErrors.LAST_NAME_SPECIAL_CHARACTERS), + phoneNumber: phoneValidator( + multiroomErrors.PHONE_REQUIRED, + multiroomErrors.PHONE_REQUESTED + ), membershipNo: z .string() .optional() @@ -34,12 +46,12 @@ export const multiroomDetailsSchema = z.object({ return !val.match(/[^0-9]/g) } return true - }, "Only digits are allowed") + }, multiroomErrors.MEMBERSHIP_NO_ONLY_DIGITS) .refine((num) => { if (num) { return num.match(/^30812(?!(0|1|2))[0-9]{9}$/) } return true - }, "Invalid membership number format"), + }, multiroomErrors.MEMBERSHIP_NO_INVALID), specialRequest: specialRequestSchema, }) diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts index f88020503..10e1863f1 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Details/RoomOne/schema.ts @@ -11,22 +11,37 @@ const stringMatcher = const isValidString = (key: string) => stringMatcher.test(key) +export const roomOneErrors = { + COUNTRY_REQUIRED: "COUNTRY_REQUIRED", + FIRST_NAME_REQUIRED: "FIRST_NAME_REQUIRED", + FIRST_NAME_SPECIAL_CHARACTERS: "FIRST_NAME_SPECIAL_CHARACTERS", + LAST_NAME_REQUIRED: "LAST_NAME_REQUIRED", + LAST_NAME_SPECIAL_CHARACTERS: "LAST_NAME_SPECIAL_CHARACTERS", + PHONE_REQUIRED: "PHONE_REQUIRED", + PHONE_REQUESTED: "PHONE_REQUESTED", + EMAIL_REQUIRED: "EMAIL_REQUIRED", + MEMBERSHIP_NO_ONLY_DIGITS: "MEMBERSHIP_NO_ONLY_DIGITS", + MEMBERSHIP_NO_INVALID: "MEMBERSHIP_NO_INVALID", + ZIP_CODE_REQUIRED: "ZIP_CODE_REQUIRED", + BIRTH_DATE_REQUIRED: "BIRTH_DATE_REQUIRED", + BIRTH_DATE_AGE_18: "BIRTH_DATE_AGE_18", +} as const + export const baseDetailsSchema = z.object({ - countryCode: z.string().min(1, { message: "Country is required" }), - email: z.string().email({ message: "Email address is required" }), + countryCode: z.string().min(1, roomOneErrors.COUNTRY_REQUIRED), + email: z.string().email(roomOneErrors.EMAIL_REQUIRED), firstName: z .string() - .min(1, { message: "First name is required" }) - .refine(isValidString, { - message: "First name can't contain any special characters", - }), + .min(1, roomOneErrors.FIRST_NAME_REQUIRED) + .refine(isValidString, roomOneErrors.FIRST_NAME_SPECIAL_CHARACTERS), lastName: z .string() - .min(1, { message: "Last name is required" }) - .refine(isValidString, { - message: "Last name can't contain any special characters", - }), - phoneNumber: phoneValidator(), + .min(1, roomOneErrors.LAST_NAME_REQUIRED) + .refine(isValidString, roomOneErrors.LAST_NAME_SPECIAL_CHARACTERS), + phoneNumber: phoneValidator( + roomOneErrors.PHONE_REQUIRED, + roomOneErrors.PHONE_REQUESTED + ), specialRequest: specialRequestSchema, }) @@ -43,32 +58,29 @@ export const notJoinDetailsSchema = baseDetailsSchema.merge( return !val.match(/[^0-9]/g) } return true - }, "Only digits are allowed") + }, roomOneErrors.MEMBERSHIP_NO_ONLY_DIGITS) .refine((num) => { if (num) { return num.match(/^30812(?!(0|1|2))[0-9]{9}$/) } return true - }, "Invalid membership number format"), + }, roomOneErrors.MEMBERSHIP_NO_INVALID), }) ) export const joinDetailsSchema = baseDetailsSchema.merge( z.object({ join: z.literal(true), - zipCode: z.string().min(1, { message: "Zip code is required" }), + zipCode: z.string().min(1, roomOneErrors.ZIP_CODE_REQUIRED), dateOfBirth: z .string() - .min(1, { message: "Date of birth is required" }) - .refine( - (date) => { - const today = dt() - const dob = dt(date) - const age = today.diff(dob, "year") - return age >= 18 - }, - { message: "Must be at least 18 years of age to continue" } - ), + .min(1, roomOneErrors.BIRTH_DATE_REQUIRED) + .refine((date) => { + const today = dt() + const dob = dt(date) + const age = today.diff(dob, "year") + return age >= 18 + }, roomOneErrors.BIRTH_DATE_AGE_18), membershipNo: z.string().default(""), }) ) diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/schema.ts b/apps/scandic-web/components/HotelReservation/FindMyBooking/schema.ts index 0711ca1b8..e9bc192eb 100644 --- a/apps/scandic-web/components/HotelReservation/FindMyBooking/schema.ts +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/schema.ts @@ -1,49 +1,41 @@ -import { defineMessage } from "react-intl" import { z } from "zod" export { type AdditionalInfoFormSchema, additionalInfoFormSchema, + findMyBookingErrors, type FindMyBookingFormSchema, findMyBookingFormSchema, } -defineMessage({ - defaultMessage: "Invalid booking number", -}) -defineMessage({ - defaultMessage: "Booking number is required", -}) -defineMessage({ - defaultMessage: "First name is required", -}) -defineMessage({ - defaultMessage: "Last name is required", -}) -defineMessage({ - defaultMessage: "Email address is required", -}) +const findMyBookingErrors = { + BOOKING_NUMBER_INVALID: "BOOKING_NUMBER_INVALID", + BOOKING_NUMBER_REQUIRED: "BOOKING_NUMBER_REQUIRED", + FIRST_NAME_REQUIRED: "FIRST_NAME_REQUIRED", + LAST_NAME_REQUIRED: "LAST_NAME_REQUIRED", + EMAIL_REQUIRED: "EMAIL_REQUIRED", +} as const const additionalInfoFormSchema = z.object({ - firstName: z.string().trim().max(250).min(1, { - message: "First name is required", - }), - email: z.string().max(250).email({ message: "Email address is required" }), + firstName: z + .string() + .trim() + .max(250) + .min(1, findMyBookingErrors.FIRST_NAME_REQUIRED), + email: z.string().max(250).email(findMyBookingErrors.EMAIL_REQUIRED), }) const findMyBookingFormSchema = additionalInfoFormSchema.extend({ confirmationNumber: z .string() .trim() - .min(1, { - message: "Booking number is required", - }) - .regex(/^[0-9]+(-[0-9])?$/, { - message: "Invalid booking number", - }), - lastName: z.string().trim().max(250).min(1, { - message: "Last name is required", - }), + .min(1, findMyBookingErrors.BOOKING_NUMBER_REQUIRED) + .regex(/^[0-9]+(-[0-9])?$/, findMyBookingErrors.BOOKING_NUMBER_INVALID), + lastName: z + .string() + .trim() + .max(250) + .min(1, findMyBookingErrors.LAST_NAME_REQUIRED), }) type AdditionalInfoFormSchema = z.output diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts b/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts new file mode 100644 index 000000000..697873b40 --- /dev/null +++ b/apps/scandic-web/components/TempDesignSystem/Form/Input/errors.ts @@ -0,0 +1,122 @@ +import { editProfileErrors } from "@/components/Forms/Edit/Profile/schema" +import { signupErrors } from "@/components/Forms/Signup/schema" +import { multiroomErrors } from "@/components/HotelReservation/EnterDetails/Details/Multiroom/schema" +import { roomOneErrors } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema" +import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema" + +import type { IntlShape } from "react-intl" + +export function getErrorMessage(intl: IntlShape, errorCode?: string) { + switch (errorCode) { + case findMyBookingErrors.BOOKING_NUMBER_INVALID: + return intl.formatMessage({ + defaultMessage: "Invalid booking number", + }) + case findMyBookingErrors.BOOKING_NUMBER_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Invalid booking number", + }) + case findMyBookingErrors.FIRST_NAME_REQUIRED: + case signupErrors.FIRST_NAME_REQUIRED: + case multiroomErrors.FIRST_NAME_REQUIRED: + case roomOneErrors.FIRST_NAME_REQUIRED: + return intl.formatMessage({ + defaultMessage: "First name is required", + }) + case multiroomErrors.FIRST_NAME_SPECIAL_CHARACTERS: + case roomOneErrors.FIRST_NAME_SPECIAL_CHARACTERS: + return intl.formatMessage({ + defaultMessage: "First name can't contain any special characters", + }) + case findMyBookingErrors.LAST_NAME_REQUIRED: + case signupErrors.LAST_NAME_REQUIRED: + case multiroomErrors.LAST_NAME_REQUIRED: + case roomOneErrors.LAST_NAME_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Last name is required", + }) + case multiroomErrors.LAST_NAME_SPECIAL_CHARACTERS: + case roomOneErrors.LAST_NAME_SPECIAL_CHARACTERS: + return intl.formatMessage({ + defaultMessage: "Last name can't contain any special characters", + }) + case findMyBookingErrors.EMAIL_REQUIRED: + case multiroomErrors.EMAIL_REQUIRED: + case roomOneErrors.EMAIL_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Email address is required", + }) + case signupErrors.COUNTRY_REQUIRED: + case multiroomErrors.COUNTRY_REQUIRED: + case roomOneErrors.COUNTRY_REQUIRED: + case editProfileErrors.COUNTRY_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Country is required", + }) + case signupErrors.PHONE_REQUIRED: + case multiroomErrors.PHONE_REQUIRED: + case roomOneErrors.PHONE_REQUIRED: + case editProfileErrors.PHONE_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Phone is required", + }) + case signupErrors.PHONE_REQUESTED: + case multiroomErrors.PHONE_REQUESTED: + case roomOneErrors.PHONE_REQUESTED: + case editProfileErrors.PHONE_REQUESTED: + return intl.formatMessage({ + defaultMessage: "Please enter a valid phone number", + }) + case signupErrors.BIRTH_DATE_REQUIRED: + case roomOneErrors.BIRTH_DATE_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Date of birth is required", + }) + case roomOneErrors.BIRTH_DATE_AGE_18: + return intl.formatMessage({ + defaultMessage: "Must be at least 18 years of age to continue", + }) + case roomOneErrors.ZIP_CODE_REQUIRED: + case editProfileErrors.ZIP_CODE_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Zip code is required", + }) + case signupErrors.PASSWORD_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Password is required", + }) + case editProfileErrors.PASSWORD_NEW_REQUIRED: + return intl.formatMessage({ + defaultMessage: "New password is required", + }) + case editProfileErrors.PASSWORD_RETYPE_NEW_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Retype new password is required", + }) + case editProfileErrors.PASSWORD_CURRENT_REQUIRED: + return intl.formatMessage({ + defaultMessage: "Current password is required", + }) + case editProfileErrors.PASSWORD_NEW_NOT_MATCH: + return intl.formatMessage({ + defaultMessage: "Retype new password does not match new password", + }) + case signupErrors.TERMS_REQUIRED: + return intl.formatMessage({ + defaultMessage: "You must accept the terms and conditions", + }) + case multiroomErrors.MEMBERSHIP_NO_ONLY_DIGITS: + case roomOneErrors.MEMBERSHIP_NO_ONLY_DIGITS: + return intl.formatMessage({ + defaultMessage: "Only digits are allowed", + }) + case multiroomErrors.MEMBERSHIP_NO_INVALID: + case roomOneErrors.MEMBERSHIP_NO_INVALID: + return intl.formatMessage({ + defaultMessage: "Invalid membership number format", + }) + default: + console.warn("Error code not supported:", errorCode) + return errorCode + } +} diff --git a/apps/scandic-web/components/TempDesignSystem/Form/Input/index.tsx b/apps/scandic-web/components/TempDesignSystem/Form/Input/index.tsx index d3f88b66b..c0a04d30a 100644 --- a/apps/scandic-web/components/TempDesignSystem/Form/Input/index.tsx +++ b/apps/scandic-web/components/TempDesignSystem/Form/Input/index.tsx @@ -1,4 +1,5 @@ "use client" + import { forwardRef, type HTMLAttributes, type WheelEvent } from "react" import { Text, TextField } from "react-aria-components" import { Controller, useFormContext } from "react-hook-form" @@ -9,6 +10,8 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel" import Caption from "@/components/TempDesignSystem/Text/Caption" +import { getErrorMessage } from "./errors" + import styles from "./input.module.css" import type { InputProps } from "./input" @@ -81,10 +84,7 @@ const Input = forwardRef(function Input( {fieldState.error && !hideError ? ( - {intl.formatMessage({ - // eslint-disable-next-line formatjs/enforce-default-message - defaultMessage: fieldState.error.message, - })} + {getErrorMessage(intl, fieldState.error.message)} ) : null}