Skip to content

Commit

Permalink
th-90: Customer sign-up flow be/fe (BinaryStudioAcademy#153)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Some14u committed Sep 18, 2023
1 parent 8412e44 commit aed4e30
Show file tree
Hide file tree
Showing 74 changed files with 693 additions and 232 deletions.
1 change: 1 addition & 0 deletions backend/src/libs/packages/http/http.ts
Original file line number Diff line number Diff line change
@@ -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';
3 changes: 1 addition & 2 deletions backend/src/libs/packages/http/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 0 additions & 1 deletion backend/src/libs/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export {
type OperationResult,
type RequireProperty,
type ServerCommonErrorResponse,
type ServerErrorResponse,
type ServerValidationErrorResponse,
type ValidationSchema,
type ValueOf,
Expand Down
7 changes: 5 additions & 2 deletions backend/src/packages/auth/auth.app-plugin.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -23,7 +23,10 @@ const authPlugin = fp<AuthPluginOptions>((fastify, options, done) => {
done: (error?: Error) => void,
): Promise<void> => {
try {
const token = request.headers.authorization?.replace('Bearer ', '');
const token = request.headers[HttpHeader.AUTHORIZATION]?.replace(
'Bearer ',
'',
);

if (!token && isJwtRequired) {
return done(createUnauthorizedError(HttpMessage.UNAUTHORIZED));
Expand Down
54 changes: 24 additions & 30 deletions backend/src/packages/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,37 +63,46 @@ class AuthService {
private async checkIsExistingUser({
email,
phone,
}: CustomerSignUpRequestDto | BusinessSignUpRequestDto): Promise<boolean> {
}: CustomerSignUpRequestDto | BusinessSignUpRequestDto): Promise<void> {
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<boolean> {
}: BusinessSignUpRequestDto): Promise<void> {
const existingBusiness = await this.businessService.checkIsExistingBusiness(
{ taxNumber },
);

return Boolean(existingBusiness);
}

public async signUpCustomer(
payload: CustomerSignUpRequestDto,
): Promise<UserEntityObjectWithGroupT> {
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<UserEntityObjectWithGroupT> {
await this.checkIsExistingUser(payload);
const group = await this.groupService.findByKey(UserGroupKey.CUSTOMER);

if (!group) {
Expand All @@ -106,7 +115,6 @@ class AuthService {
...payload,
groupId: group.id,
});

const userWithToken = await this.generateAccessTokenAndUpdateUser(
newUser.id,
);
Expand All @@ -117,22 +125,8 @@ class AuthService {
public async signUpBusiness(
payload: BusinessSignUpRequestDto,
): Promise<UserEntityObjectWithGroupAndBusinessT> {
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,
Expand Down
12 changes: 8 additions & 4 deletions backend/src/packages/groups/group.entity.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
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 {
private 'id': number | null;

private 'name': string;

private 'key': string;
private 'key': UserGroupKeyT;

private constructor({
id,
Expand All @@ -17,7 +21,7 @@ class GroupEntity implements IEntity {
}: NullableProperties<GroupEntityT, 'id'>) {
this.id = id;
this.name = name;
this.key = key;
this.key = key as UserGroupKeyT;
}

public static initialize({
Expand All @@ -43,15 +47,15 @@ class GroupEntity implements IEntity {
});
}

public toObject(): GroupEntityT {
public toObject(): UserGroupEntityT {
return {
id: this.id as number,
name: this.name,
key: this.key,
};
}

public toNewObject(): Omit<GroupEntityT, 'id'> {
public toNewObject(): Omit<UserGroupEntityT, 'id'> {
return {
name: this.name,
key: this.key,
Expand Down
1 change: 0 additions & 1 deletion backend/src/packages/users/libs/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ export {
type UserGetAllResponseDto,
type UserGroupEntityT,
type UserGroupKeyT,
type UserGroupNameT,
} from 'shared/build/index.js';
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/assets/css/plugins/balloon-css.plugin.scss
Original file line number Diff line number Diff line change
@@ -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
)};
}
}
1 change: 1 addition & 0 deletions frontend/src/assets/css/plugins/plugins.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "./balloon-css.plugin.scss";
1 change: 1 addition & 0 deletions frontend/src/assets/css/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
@import "./typography.scss";
@import "./animations.scss";
@import "./scroll-bar.scss";
@import "./plugins/plugins.scss";
3 changes: 2 additions & 1 deletion frontend/src/libs/components/business-card/business-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof AuthMode>) => void;
};

const BusinessCard: React.FC<Properties> = ({ onClick }: Properties) => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/libs/components/customer-card/customer-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof AuthMode>) => void;
};

const CustomerCard: React.FC<Properties> = ({ onClick }: Properties) => {
Expand Down
53 changes: 44 additions & 9 deletions frontend/src/libs/components/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<T extends FieldValues> = {
Expand All @@ -30,15 +31,17 @@ type Properties<T extends FieldValues> = {
btnLabel?: string;
isDisabled?: boolean;
onSubmit: (payload: T) => void;
serverError?: ServerErrorHandling;
additionalFields?: JSX.Element;
};

type Parameters<T extends FieldValues = FieldValues> = {
type RenderFieldProperties<T extends FieldValues = FieldValues> = {
field: FormField<T>;
control: Control<T, null>;
errors: FieldErrors<T>;
setError: UseFormReturn<T>['setError'];
clearErrors: UseFormReturn<T>['clearErrors'];
setError: UseFormSetError<T>;
clearErrors: UseFormClearErrors<T>;
clearServerError?: ServerErrorHandling['clearError'];
};

const renderField = <T extends FieldValues = FieldValues>({
Expand All @@ -47,7 +50,8 @@ const renderField = <T extends FieldValues = FieldValues>({
errors,
setError,
clearErrors,
}: Parameters<T>): JSX.Element => {
clearServerError,
}: RenderFieldProperties<T>): JSX.Element => {
switch (field.type) {
case 'dropdown': {
const { options, name, label } = field;
Expand All @@ -66,7 +70,15 @@ const renderField = <T extends FieldValues = FieldValues>({
case 'text':
case 'email':
case 'password': {
return <Input {...field} control={control} errors={errors} />;
return (
<Input
{...field}
control={control}
errors={errors}
setError={setError}
clearServerError={clearServerError}
/>
);
}

case 'file': {
Expand All @@ -91,7 +103,14 @@ const renderField = <T extends FieldValues = FieldValues>({
}

default: {
return <Input {...field} control={control} errors={errors} />;
return (
<Input
{...field}
control={control}
errors={errors}
setError={setError}
/>
);
}
}
};
Expand All @@ -104,15 +123,24 @@ const Form = <T extends FieldValues = FieldValues>({
additionalFields,
isDisabled,
onSubmit,
serverError,
}: Properties<T>): JSX.Element => {
const { control, errors, handleSubmit, setError, clearErrors } =
useAppForm<T>({
defaultValues,
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],
Expand All @@ -121,7 +149,14 @@ const Form = <T extends FieldValues = FieldValues>({
const createInputs = (): JSX.Element[] => {
return fields.map((field, index) => (
<div key={(field.id = index)}>
{renderField({ field, control, errors, setError, clearErrors })}
{renderField({
field,
control,
errors,
setError,
clearErrors,
clearServerError: serverError?.clearError,
})}
</div>
));
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/libs/components/form/libs/consts/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FIELD_PATH_DELIMITER } from './field-path-delimiter.const.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const FIELD_PATH_DELIMITER = '.';

export { FIELD_PATH_DELIMITER };
1 change: 1 addition & 0 deletions frontend/src/libs/components/form/libs/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ServerErrorSymbol } from './server-error-symbol.enum.js';
Loading

0 comments on commit aed4e30

Please sign in to comment.