From aed4e30253c0a10f2212ed9574b83833bf8f8fdd Mon Sep 17 00:00:00 2001 From: Some14u <81959259+Some14u@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:17:57 +0300 Subject: [PATCH] th-90: Customer sign-up flow be/fe (#153) * th-90: + various improvements * th-90: * fix the behaviour of header sign-in button and add default navigate after sign-in * th-90: * fix frontend issues after merge with development * th-90: * fix backend routing for customer sign-up * th-90: * fix dissapearing background image in sign-up * th-90: * prettify some types * th-90: + add frontend support of server errors for forms * th-90: * fix tsconfig in shared folder * th-90: + add sign-up button to header * th-90: * change backend auth handler to send distinct errors on violation of email/phone constraints * th-90: - remove unused import * th-90: * temporarily revert constraint constants from the tables-schema * th-90: * move database unique violation error handling to separate error, small fixes * th-90: * resolve problems after merge developement * th-90: * change user type inside auth slice, various minor fixes after code review * th-90: * more minor fixes * th-90: * change user.group to user.group.key * th-90: * fix issues after code-review * th-90: + add error popups near the form elements on validation error, more fixes according to code review * th-90: + repeat of the previous commit * th-90: * fix issue with error appearence control * th-90: - remove thunk serializer wrapper and use rejectWithValue instead * th-90: * rework error handling to use redux state instead of direct interception * th-90: * make proper universal error handling hook and extract logic from the form component * th-90: * cosmetic improvements * th-90: * add enum to filename * th-90: * make error in slice possible to be null instead of undefined, other minor fixes * th-90: * more minor improvements according to review * th-90: * update auth component logic * th-90: - remove redundant useEffect * th-90: + add knexfile back and fix dependencies inside input component --- backend/src/libs/packages/http/http.ts | 1 + .../libs/packages/http/libs/enums/enums.ts | 3 +- backend/src/libs/types/types.ts | 1 - backend/src/packages/auth/auth.app-plugin.ts | 7 +- backend/src/packages/auth/auth.service.ts | 54 +++++------- backend/src/packages/groups/group.entity.ts | 12 ++- .../src/packages/users/libs/types/types.ts | 1 - frontend/package.json | 1 + .../css/plugins/balloon-css.plugin.scss | 30 +++++++ frontend/src/assets/css/plugins/plugins.scss | 1 + frontend/src/assets/css/styles.scss | 1 + .../business-card/business-card.tsx | 3 +- .../customer-card/customer-card.tsx | 3 +- frontend/src/libs/components/form/form.tsx | 53 +++++++++-- .../components/form/libs/consts/consts.ts | 1 + .../libs/consts/field-path-delimiter.const.ts | 3 + .../libs/components/form/libs/enums/enums.ts | 1 + .../libs/enums/server-error-symbol.enum.ts | 6 ++ .../helpers/handle-server-error.helper.ts | 88 +++++++++++++++++++ .../components/form/libs/helpers/helpers.ts | 1 + .../src/libs/components/header/header.tsx | 52 +++++++---- .../components/header/libs/helpers/helpers.ts | 1 + .../libs/components/header/styles.module.scss | 12 ++- frontend/src/libs/components/input/input.tsx | 38 +++++++- .../protected-route/protected-route.tsx | 5 +- .../src/libs/components/router/router.tsx | 3 +- frontend/src/libs/enums/app-route.enum.ts | 3 +- frontend/src/libs/enums/enums.ts | 1 + frontend/src/libs/hooks/hooks.ts | 4 +- .../use-app-dispatch/use-app-dispatch.hook.ts | 6 +- .../hooks/use-app-form/use-app-form.hook.ts | 13 +-- .../use-app-selector/use-app-selector.hook.ts | 6 +- .../use-form-server-error.hook.ts | 29 ++++++ .../use-server-error-from-thunk.hook.ts | 37 ++++++++ frontend/src/libs/packages/api/http-api.ts | 1 - .../packages/store/libs/types/store.types.ts | 30 +++++++ .../libs/packages/store/libs/types/types.ts | 1 + .../src/libs/packages/store/store.package.ts | 18 +--- frontend/src/libs/packages/store/store.ts | 7 ++ frontend/src/libs/types/app-think.type.ts | 4 +- .../src/libs/types/async-thunk-config.type.ts | 10 ++- frontend/src/libs/types/form.type.ts | 24 ++++- .../libs/types/server-error-handling.type.ts | 8 ++ frontend/src/libs/types/store.type.ts | 5 -- frontend/src/libs/types/types.ts | 7 +- frontend/src/pages/auth/auth.tsx | 70 ++++++++++----- .../components/sign-in-form/libs/fields.ts | 4 +- .../components/sign-in-form/sign-in-form.tsx | 8 +- .../components/sign-up-form/libs/fields.ts | 6 +- .../components/sign-up-form/sign-up-form.tsx | 14 ++- frontend/src/pages/auth/libs/hooks/hooks.ts | 1 + .../use-auth-navigate.hook.ts | 0 frontend/src/pages/welcome/welcome.tsx | 20 ++++- frontend/src/slices/auth/actions.ts | 39 ++++---- frontend/src/slices/auth/auth.slice.ts | 18 +++- frontend/src/slices/auth/auth.ts | 1 + frontend/src/slices/auth/libs/hooks/hooks.ts | 2 + .../libs/hooks/use-auth-sever-error.hook.ts | 14 +++ .../auth/libs/hooks/use-auth-user.hook.ts | 9 ++ frontend/src/slices/auth/selectors.ts | 25 +++--- package-lock.json | 35 ++++++++ shared/package.json | 4 + shared/src/index.ts | 4 +- .../get-full-name/get-full-name.helper.ts | 7 ++ shared/src/libs/helpers/helpers.ts | 1 + .../libs/enums/http-error-message.enum.ts | 7 +- shared/src/libs/types/capitalize-enum.type.ts | 5 -- shared/src/libs/types/types.ts | 1 - .../auth/libs/enums/auth-api-path.enum.ts | 1 - shared/src/packages/users/libs/types/types.ts | 6 +- .../users/libs/types/user-entity.type.ts | 4 +- .../libs/types/user-group-entity.type.ts | 14 +-- .../users/libs/types/user-group-name.type.ts | 7 -- shared/src/packages/users/users.ts | 2 - 74 files changed, 693 insertions(+), 232 deletions(-) create mode 100644 frontend/src/assets/css/plugins/balloon-css.plugin.scss create mode 100644 frontend/src/assets/css/plugins/plugins.scss create mode 100644 frontend/src/libs/components/form/libs/consts/consts.ts create mode 100644 frontend/src/libs/components/form/libs/consts/field-path-delimiter.const.ts create mode 100644 frontend/src/libs/components/form/libs/enums/enums.ts create mode 100644 frontend/src/libs/components/form/libs/enums/server-error-symbol.enum.ts create mode 100644 frontend/src/libs/components/form/libs/helpers/handle-server-error.helper.ts create mode 100644 frontend/src/libs/components/form/libs/helpers/helpers.ts create mode 100644 frontend/src/libs/hooks/use-form-server-error/use-form-server-error.hook.ts create mode 100644 frontend/src/libs/hooks/use-server-error-from-thunk/use-server-error-from-thunk.hook.ts create mode 100644 frontend/src/libs/packages/store/libs/types/store.types.ts create mode 100644 frontend/src/libs/packages/store/libs/types/types.ts create mode 100644 frontend/src/libs/types/server-error-handling.type.ts delete mode 100644 frontend/src/libs/types/store.type.ts create mode 100644 frontend/src/pages/auth/libs/hooks/hooks.ts rename frontend/src/{ => pages/auth}/libs/hooks/use-auth-navigate/use-auth-navigate.hook.ts (100%) create mode 100644 frontend/src/slices/auth/libs/hooks/hooks.ts create mode 100644 frontend/src/slices/auth/libs/hooks/use-auth-sever-error.hook.ts create mode 100644 frontend/src/slices/auth/libs/hooks/use-auth-user.hook.ts create mode 100644 shared/src/libs/helpers/get-full-name/get-full-name.helper.ts delete mode 100644 shared/src/libs/types/capitalize-enum.type.ts delete mode 100644 shared/src/packages/users/libs/types/user-group-name.type.ts diff --git a/backend/src/libs/packages/http/http.ts b/backend/src/libs/packages/http/http.ts index f5d299e4d..130a8a669 100644 --- a/backend/src/libs/packages/http/http.ts +++ b/backend/src/libs/packages/http/http.ts @@ -1,4 +1,5 @@ export { HttpCode } from './libs/enums/enums.js'; export { HttpMessage } from './libs/enums/enums.js'; +export { HttpHeader } from './libs/enums/enums.js'; export { HttpError } from './libs/exceptions/exceptions.js'; export { type HttpMethod } from './libs/types/types.js'; diff --git a/backend/src/libs/packages/http/libs/enums/enums.ts b/backend/src/libs/packages/http/libs/enums/enums.ts index a88f2b1b0..344ebf5bb 100644 --- a/backend/src/libs/packages/http/libs/enums/enums.ts +++ b/backend/src/libs/packages/http/libs/enums/enums.ts @@ -1,2 +1 @@ -export { HttpCode } from 'shared/build/index.js'; -export { HttpMessage } from 'shared/build/index.js'; +export { HttpCode, HttpHeader, HttpMessage } from 'shared/build/index.js'; diff --git a/backend/src/libs/types/types.ts b/backend/src/libs/types/types.ts index 24a31d782..70f58af2c 100644 --- a/backend/src/libs/types/types.ts +++ b/backend/src/libs/types/types.ts @@ -6,7 +6,6 @@ export { type OperationResult, type RequireProperty, type ServerCommonErrorResponse, - type ServerErrorResponse, type ServerValidationErrorResponse, type ValidationSchema, type ValueOf, diff --git a/backend/src/packages/auth/auth.app-plugin.ts b/backend/src/packages/auth/auth.app-plugin.ts index 079bf8a4d..f15d725f7 100644 --- a/backend/src/packages/auth/auth.app-plugin.ts +++ b/backend/src/packages/auth/auth.app-plugin.ts @@ -1,7 +1,7 @@ import { type FastifyReply, type FastifyRequest } from 'fastify'; import fp from 'fastify-plugin'; -import { HttpMessage } from '~/libs/packages/http/http.js'; +import { HttpHeader, HttpMessage } from '~/libs/packages/http/http.js'; import { type ValueOf } from '~/libs/types/types.js'; import { AuthStrategy } from './auth.js'; @@ -23,7 +23,10 @@ const authPlugin = fp((fastify, options, done) => { done: (error?: Error) => void, ): Promise => { try { - const token = request.headers.authorization?.replace('Bearer ', ''); + const token = request.headers[HttpHeader.AUTHORIZATION]?.replace( + 'Bearer ', + '', + ); if (!token && isJwtRequired) { return done(createUnauthorizedError(HttpMessage.UNAUTHORIZED)); diff --git a/backend/src/packages/auth/auth.service.ts b/backend/src/packages/auth/auth.service.ts index 0a9cf7632..1b891ed10 100644 --- a/backend/src/packages/auth/auth.service.ts +++ b/backend/src/packages/auth/auth.service.ts @@ -63,37 +63,46 @@ class AuthService { private async checkIsExistingUser({ email, phone, - }: CustomerSignUpRequestDto | BusinessSignUpRequestDto): Promise { + }: CustomerSignUpRequestDto | BusinessSignUpRequestDto): Promise { const existingUser = await this.userService.findByPhoneOrEmail( phone, email, ); - return Boolean(existingUser); + if (email === existingUser?.email) { + throw new HttpError({ + message: HttpMessage.USER_EMAIL_EXISTS, + status: HttpCode.CONFLICT, + }); + } + + if (phone === existingUser?.phone) { + throw new HttpError({ + message: HttpMessage.USER_PHONE_EXISTS, + status: HttpCode.CONFLICT, + }); + } } private async checkIsExistingBusiness({ taxNumber, - }: BusinessSignUpRequestDto): Promise { + }: BusinessSignUpRequestDto): Promise { const existingBusiness = await this.businessService.checkIsExistingBusiness( { taxNumber }, ); - return Boolean(existingBusiness); - } - - public async signUpCustomer( - payload: CustomerSignUpRequestDto, - ): Promise { - const isExistingUser = await this.checkIsExistingUser(payload); - - if (isExistingUser) { + if (existingBusiness) { throw new HttpError({ - message: HttpMessage.USER_EXISTS, + message: HttpMessage.BUSINESS_EXISTS, status: HttpCode.CONFLICT, }); } + } + public async signUpCustomer( + payload: CustomerSignUpRequestDto, + ): Promise { + await this.checkIsExistingUser(payload); const group = await this.groupService.findByKey(UserGroupKey.CUSTOMER); if (!group) { @@ -106,7 +115,6 @@ class AuthService { ...payload, groupId: group.id, }); - const userWithToken = await this.generateAccessTokenAndUpdateUser( newUser.id, ); @@ -117,22 +125,8 @@ class AuthService { public async signUpBusiness( payload: BusinessSignUpRequestDto, ): Promise { - const isExistingUser = await this.checkIsExistingUser(payload); - const isExistingBusiness = await this.checkIsExistingBusiness(payload); - - if (isExistingUser) { - throw new HttpError({ - message: HttpMessage.USER_EXISTS, - status: HttpCode.CONFLICT, - }); - } - - if (isExistingBusiness) { - throw new HttpError({ - message: HttpMessage.BUSINESS_EXISTS, - status: HttpCode.CONFLICT, - }); - } + await this.checkIsExistingUser(payload); + await this.checkIsExistingBusiness(payload); const { phone, diff --git a/backend/src/packages/groups/group.entity.ts b/backend/src/packages/groups/group.entity.ts index bd6f4093a..b464a64f2 100644 --- a/backend/src/packages/groups/group.entity.ts +++ b/backend/src/packages/groups/group.entity.ts @@ -1,6 +1,10 @@ import { type IEntity } from '~/libs/interfaces/interfaces.js'; import { type NullableProperties } from '~/libs/types/types.js'; +import { + type UserGroupEntityT, + type UserGroupKeyT, +} from '../users/libs/types/types.js'; import { type GroupEntityT } from './libs/types/types.js'; class GroupEntity implements IEntity { @@ -8,7 +12,7 @@ class GroupEntity implements IEntity { private 'name': string; - private 'key': string; + private 'key': UserGroupKeyT; private constructor({ id, @@ -17,7 +21,7 @@ class GroupEntity implements IEntity { }: NullableProperties) { this.id = id; this.name = name; - this.key = key; + this.key = key as UserGroupKeyT; } public static initialize({ @@ -43,7 +47,7 @@ class GroupEntity implements IEntity { }); } - public toObject(): GroupEntityT { + public toObject(): UserGroupEntityT { return { id: this.id as number, name: this.name, @@ -51,7 +55,7 @@ class GroupEntity implements IEntity { }; } - public toNewObject(): Omit { + public toNewObject(): Omit { return { name: this.name, key: this.key, diff --git a/backend/src/packages/users/libs/types/types.ts b/backend/src/packages/users/libs/types/types.ts index 866dc9c06..51fd9bc75 100644 --- a/backend/src/packages/users/libs/types/types.ts +++ b/backend/src/packages/users/libs/types/types.ts @@ -16,5 +16,4 @@ export { type UserGetAllResponseDto, type UserGroupEntityT, type UserGroupKeyT, - type UserGroupNameT, } from 'shared/build/index.js'; diff --git a/frontend/package.json b/frontend/package.json index 0900cc4ad..b7b6efaeb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@rollup/pluginutils": "5.0.4", "@svgr/core": "8.1.0", "@tanstack/react-table": "8.9.3", + "balloon-css": "1.2.0", "clsx": "2.0.0", "modern-normalize": "2.0.0", "react": "18.2.0", diff --git a/frontend/src/assets/css/plugins/balloon-css.plugin.scss b/frontend/src/assets/css/plugins/balloon-css.plugin.scss new file mode 100644 index 000000000..5598f6967 --- /dev/null +++ b/frontend/src/assets/css/plugins/balloon-css.plugin.scss @@ -0,0 +1,30 @@ +@use "sass:color"; + +@import "../vars.scss"; +@import "balloon-css/src/balloon.scss"; + +:root [aria-label][data-balloon-pos] { + --balloon-border-radius: 4px; + --balloon-color: #{color.adjust($red-dark, $lightness: -10%)}; + --balloon-font-size: 16px; + --balloon-move: 30px; + --balloon-text-color: #{$white}; + + &::before { + right: 8px; + z-index: 11; + } + + &::after { + right: 4px; + max-width: calc(100% - 8px); + font-weight: $font-weight-regular; + font-family: $font-family; + white-space: normal; + box-shadow: 0 3px 6px 0 #{color.adjust( + $red-dark, + $lightness: - 30%, + $alpha: - 0.5 + )}; + } +} diff --git a/frontend/src/assets/css/plugins/plugins.scss b/frontend/src/assets/css/plugins/plugins.scss new file mode 100644 index 000000000..9f9d9f24b --- /dev/null +++ b/frontend/src/assets/css/plugins/plugins.scss @@ -0,0 +1 @@ +@import "./balloon-css.plugin.scss"; diff --git a/frontend/src/assets/css/styles.scss b/frontend/src/assets/css/styles.scss index 3eb5a9340..db73155ee 100644 --- a/frontend/src/assets/css/styles.scss +++ b/frontend/src/assets/css/styles.scss @@ -4,3 +4,4 @@ @import "./typography.scss"; @import "./animations.scss"; @import "./scroll-bar.scss"; +@import "./plugins/plugins.scss"; diff --git a/frontend/src/libs/components/business-card/business-card.tsx b/frontend/src/libs/components/business-card/business-card.tsx index 528830e96..3b3b72784 100644 --- a/frontend/src/libs/components/business-card/business-card.tsx +++ b/frontend/src/libs/components/business-card/business-card.tsx @@ -2,12 +2,13 @@ import rocket from '~/assets/img/welcome-page/rocket.png'; import { AuthMode } from '~/libs/enums/enums.js'; import { getValidClassNames } from '~/libs/helpers/helpers.js'; import { useCallback } from '~/libs/hooks/hooks.js'; +import { type ValueOf } from '~/libs/types/types.js'; import { Button, Image } from '../components.js'; import styles from './styles.module.scss'; type Properties = { - onClick: (mode: string) => void; + onClick: (mode: ValueOf) => void; }; const BusinessCard: React.FC = ({ onClick }: Properties) => { diff --git a/frontend/src/libs/components/customer-card/customer-card.tsx b/frontend/src/libs/components/customer-card/customer-card.tsx index fa01db303..9f44ec1a9 100644 --- a/frontend/src/libs/components/customer-card/customer-card.tsx +++ b/frontend/src/libs/components/customer-card/customer-card.tsx @@ -2,12 +2,13 @@ import customer from '~/assets/img/welcome-page/customer.png'; import { AuthMode } from '~/libs/enums/enums.js'; import { getValidClassNames } from '~/libs/helpers/helpers.js'; import { useCallback } from '~/libs/hooks/hooks.js'; +import { type ValueOf } from '~/libs/types/types.js'; import { Button, Image } from '../components.js'; import styles from './styles.module.scss'; type Properties = { - onClick: (mode: string) => void; + onClick: (mode: ValueOf) => void; }; const CustomerCard: React.FC = ({ onClick }: Properties) => { diff --git a/frontend/src/libs/components/form/form.tsx b/frontend/src/libs/components/form/form.tsx index 83de1e079..393f29e44 100644 --- a/frontend/src/libs/components/form/form.tsx +++ b/frontend/src/libs/components/form/form.tsx @@ -2,15 +2,15 @@ import { type Control, type FieldErrors, type UseFormClearErrors, - type UseFormReturn, type UseFormSetError, } from 'react-hook-form'; -import { useAppForm, useCallback } from '~/libs/hooks/hooks.js'; +import { useAppForm, useCallback, useEffect } from '~/libs/hooks/hooks.js'; import { type DeepPartial, type FieldValues, type FormField, + type ServerErrorHandling, type ValidationSchema, } from '~/libs/types/types.js'; @@ -21,6 +21,7 @@ import { fileInputDefaultsConfig } from '../file-input/libs/config/config.js'; import { type FileFormType } from '../file-input/libs/types/types.js'; import { Input } from '../input/input.jsx'; import { LocationInput } from '../location-input/location-input.js'; +import { handleServerError } from './libs/helpers/handle-server-error.helper.js'; import styles from './styles.module.scss'; type Properties = { @@ -30,15 +31,17 @@ type Properties = { btnLabel?: string; isDisabled?: boolean; onSubmit: (payload: T) => void; + serverError?: ServerErrorHandling; additionalFields?: JSX.Element; }; -type Parameters = { +type RenderFieldProperties = { field: FormField; control: Control; errors: FieldErrors; - setError: UseFormReturn['setError']; - clearErrors: UseFormReturn['clearErrors']; + setError: UseFormSetError; + clearErrors: UseFormClearErrors; + clearServerError?: ServerErrorHandling['clearError']; }; const renderField = ({ @@ -47,7 +50,8 @@ const renderField = ({ errors, setError, clearErrors, -}: Parameters): JSX.Element => { + clearServerError, +}: RenderFieldProperties): JSX.Element => { switch (field.type) { case 'dropdown': { const { options, name, label } = field; @@ -66,7 +70,15 @@ const renderField = ({ case 'text': case 'email': case 'password': { - return ; + return ( + + ); } case 'file': { @@ -91,7 +103,14 @@ const renderField = ({ } default: { - return ; + return ( + + ); } } }; @@ -104,6 +123,7 @@ const Form = ({ additionalFields, isDisabled, onSubmit, + serverError, }: Properties): JSX.Element => { const { control, errors, handleSubmit, setError, clearErrors } = useAppForm({ @@ -111,8 +131,16 @@ const Form = ({ validationSchema, }); + useEffect(() => { + if (serverError?.error) { + handleServerError(serverError.error, setError, fields); + } + }, [fields, serverError?.error, setError]); + const handleFormSubmit = useCallback( (event_: React.BaseSyntheticEvent): void => { + event_.preventDefault(); + void handleSubmit(onSubmit)(event_); }, [handleSubmit, onSubmit], @@ -121,7 +149,14 @@ const Form = ({ const createInputs = (): JSX.Element[] => { return fields.map((field, index) => (
- {renderField({ field, control, errors, setError, clearErrors })} + {renderField({ + field, + control, + errors, + setError, + clearErrors, + clearServerError: serverError?.clearError, + })}
)); }; diff --git a/frontend/src/libs/components/form/libs/consts/consts.ts b/frontend/src/libs/components/form/libs/consts/consts.ts new file mode 100644 index 000000000..cc570fe3c --- /dev/null +++ b/frontend/src/libs/components/form/libs/consts/consts.ts @@ -0,0 +1 @@ +export { FIELD_PATH_DELIMITER } from './field-path-delimiter.const.js'; diff --git a/frontend/src/libs/components/form/libs/consts/field-path-delimiter.const.ts b/frontend/src/libs/components/form/libs/consts/field-path-delimiter.const.ts new file mode 100644 index 000000000..acf2e3f24 --- /dev/null +++ b/frontend/src/libs/components/form/libs/consts/field-path-delimiter.const.ts @@ -0,0 +1,3 @@ +const FIELD_PATH_DELIMITER = '.'; + +export { FIELD_PATH_DELIMITER }; diff --git a/frontend/src/libs/components/form/libs/enums/enums.ts b/frontend/src/libs/components/form/libs/enums/enums.ts new file mode 100644 index 000000000..e37245b86 --- /dev/null +++ b/frontend/src/libs/components/form/libs/enums/enums.ts @@ -0,0 +1 @@ +export { ServerErrorSymbol } from './server-error-symbol.enum.js'; diff --git a/frontend/src/libs/components/form/libs/enums/server-error-symbol.enum.ts b/frontend/src/libs/components/form/libs/enums/server-error-symbol.enum.ts new file mode 100644 index 000000000..44fbd5b04 --- /dev/null +++ b/frontend/src/libs/components/form/libs/enums/server-error-symbol.enum.ts @@ -0,0 +1,6 @@ +const ServerErrorSymbol = { + COMMON: 'server-common', + VALIDATION: 'server-validation', +} as const; + +export { ServerErrorSymbol }; diff --git a/frontend/src/libs/components/form/libs/helpers/handle-server-error.helper.ts b/frontend/src/libs/components/form/libs/helpers/handle-server-error.helper.ts new file mode 100644 index 000000000..7f76f5c85 --- /dev/null +++ b/frontend/src/libs/components/form/libs/helpers/handle-server-error.helper.ts @@ -0,0 +1,88 @@ +import { + type FieldPath, + type FieldValues, + type UseFormSetError, +} from 'react-hook-form'; + +import { ServerErrorType } from '~/libs/enums/enums.js'; +import { type HttpError } from '~/libs/packages/http/http.js'; +import { type FormField } from '~/libs/types/types.js'; + +import { FIELD_PATH_DELIMITER } from '../consts/consts.js'; +import { ServerErrorSymbol } from '../enums/enums.js'; + +const handleServerError = ( + error: HttpError, + setError: UseFormSetError, + fields: FormField[], +): void => { + if (!('errorType' in error)) { + return; + } + + switch (error.errorType) { + case ServerErrorType.COMMON: { + assignCommonErrors(fields, error, setError); + break; + } + case ServerErrorType.VALIDATION: { + assignValidationErrors(fields, error, setError); + } + } +}; + +const assignCommonError = ( + field: FormField, + commonError: Partial, + setError: UseFormSetError, +): void => { + if (field.associateServerErrors) { + for (const errorDescriptor of field.associateServerErrors) { + if ( + typeof errorDescriptor === 'object' && + errorDescriptor.errorMessage === commonError.message + ) { + setError(field.name, errorDescriptor.error, errorDescriptor.options); + } else if (errorDescriptor === commonError.message) { + setError(field.name, { + type: ServerErrorSymbol.COMMON, + message: errorDescriptor, + }); + } + } + } +}; + +const assignCommonErrors = ( + fields: FormField[], + commonError: Partial, + setError: UseFormSetError, +): void => { + for (const field of fields) { + assignCommonError(field, commonError, setError); + } +}; + +const assignValidationErrors = ( + fields: FormField[], + commonError: Partial, + setError: UseFormSetError, +): void => { + if (!commonError.details) { + return; + } + for (const { path, message } of commonError.details) { + const fieldName = path.join(FIELD_PATH_DELIMITER); + + const fieldsHasName = fields.some((field) => field.name === fieldName); + + if (fieldsHasName) { + setError(fieldName as FieldPath, { + type: ServerErrorSymbol.VALIDATION, + message, + }); + } + } +}; + +export { handleServerError }; diff --git a/frontend/src/libs/components/form/libs/helpers/helpers.ts b/frontend/src/libs/components/form/libs/helpers/helpers.ts new file mode 100644 index 000000000..79f115199 --- /dev/null +++ b/frontend/src/libs/components/form/libs/helpers/helpers.ts @@ -0,0 +1 @@ +export { handleServerError } from './handle-server-error.helper.js'; diff --git a/frontend/src/libs/components/header/header.tsx b/frontend/src/libs/components/header/header.tsx index 21185756f..eaf84571a 100644 --- a/frontend/src/libs/components/header/header.tsx +++ b/frontend/src/libs/components/header/header.tsx @@ -1,25 +1,26 @@ import { AppRoute } from '~/libs/enums/enums.js'; -import { - useAppSelector, - useCallback, - useNavigate, -} from '~/libs/hooks/hooks.js'; -import { selectUser } from '~/slices/auth/selectors.js'; +import { getValidClassNames } from '~/libs/helpers/helpers.js'; +import { useCallback, useNavigate } from '~/libs/hooks/hooks.js'; +import { useAuthUser } from '~/slices/auth/auth.js'; import { AppLogo, BurgerMenu, Button, Link } from '../components.js'; -import { getBurgerMenuItems } from './libs/helpers/helpers.js'; +import { getBurgerMenuItems, getFullName } from './libs/helpers/helpers.js'; import styles from './styles.module.scss'; const Header: React.FC = () => { + const user = useAuthUser(); + const navigate = useNavigate(); - const user = useAppSelector(selectUser); - const hasUser = Boolean(user); + + const burgerItems = getBurgerMenuItems(user?.group.key ?? null); const handleSignIn = useCallback(() => { - navigate(AppRoute.WELCOME); + navigate(AppRoute.SIGN_IN); }, [navigate]); - const burgerItems = getBurgerMenuItems(user?.group.key ?? null); + const handleSignUp = useCallback(() => { + navigate(AppRoute.WELCOME); + }, [navigate]); return (
@@ -28,15 +29,28 @@ const Header: React.FC = () => {
- {hasUser ? ( - + {user ? ( + <> +
+ Hello, {getFullName(user.firstName, user.lastName)} +
+ + ) : ( -
diff --git a/frontend/src/libs/components/header/libs/helpers/helpers.ts b/frontend/src/libs/components/header/libs/helpers/helpers.ts index 02c36458f..18163228b 100644 --- a/frontend/src/libs/components/header/libs/helpers/helpers.ts +++ b/frontend/src/libs/components/header/libs/helpers/helpers.ts @@ -1 +1,2 @@ export { getBurgerMenuItems } from './get-burger-menu-items.helper.js'; +export { getFullName } from 'shared/build/index.js'; diff --git a/frontend/src/libs/components/header/styles.module.scss b/frontend/src/libs/components/header/styles.module.scss index 999bdd0a6..e4fd60432 100644 --- a/frontend/src/libs/components/header/styles.module.scss +++ b/frontend/src/libs/components/header/styles.module.scss @@ -18,6 +18,16 @@ width: 186px; } -.btn { +.button { padding-inline: 60px; } + +.navMenu { + display: flex; + align-items: center; + gap: 16px; +} + +.welcome { + color: $white; +} diff --git a/frontend/src/libs/components/input/input.tsx b/frontend/src/libs/components/input/input.tsx index 837f7b22d..74d56deec 100644 --- a/frontend/src/libs/components/input/input.tsx +++ b/frontend/src/libs/components/input/input.tsx @@ -3,6 +3,7 @@ import { type FieldErrors, type FieldPath, type FieldValues, + type UseFormSetError, } from 'react-hook-form'; import { type InputType } from '~/libs/enums/enums.js'; @@ -11,9 +12,10 @@ import { getValidClassNames } from '~/libs/helpers/helpers.js'; import { useCallback, useFormController, + useFormServerError, useState, } from '~/libs/hooks/hooks.js'; -import { type ValueOf } from '~/libs/types/types.js'; +import { type ServerErrorHandling, type ValueOf } from '~/libs/types/types.js'; import { Icon } from '../components.js'; import styles from './styles.module.scss'; @@ -29,6 +31,8 @@ type Properties = { min?: number; max?: number; step?: number; + setError?: UseFormSetError; + clearServerError?: ServerErrorHandling['clearError']; }; const Input = ({ @@ -42,10 +46,13 @@ const Input = ({ min, max, step, + setError, + clearServerError, }: Properties): JSX.Element => { const { field } = useFormController({ name, control }); const [isPasswordShown, setIsPasswordShown] = useState(false); const error = errors[name]?.message; + const serverError = useFormServerError(errors[name]); const hasError = Boolean(error); const hasValue = Boolean(field.value); const hasLabel = Boolean(label); @@ -64,10 +71,34 @@ const Input = ({ [isPasswordShown], ); + const clearError = useCallback(() => { + if (setError && (serverError.common || serverError.validation)) { + setError(name, {}); + + if (clearServerError) { + clearServerError(); + } + } + }, [clearServerError, name, serverError, setError]); + + const handleInputChange = useCallback( + (event: React.ChangeEvent): void => { + field.onChange(event); + + clearError(); + }, + [clearError, field], + ); + return (