Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LIME-69: Add Profile Settings #151

Merged
merged 62 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
c0974e5
LIME-69: + add profile settings
valery-chumak Feb 16, 2024
e15a9ab
LIME-69: + add styles for profile
valery-chumak Feb 16, 2024
32e6191
LIME-69: + fix styles for profile
valery-chumak Feb 16, 2024
284b119
LIME-69: * create basic layout
valery-chumak Feb 16, 2024
7f86ff6
LIME-69: * edit styles
valery-chumak Feb 17, 2024
ca7ee04
LIME-69: + merge development
valery-chumak Feb 17, 2024
3fcd363
LIME-69: * fixed validation gender
valery-chumak Feb 17, 2024
87de412
LIME-69: + add updateUser action
valery-chumak Feb 19, 2024
a637d6b
LIME-69: + solve merge conflicts
valery-chumak Feb 19, 2024
ab344fd
LIME-69: + avatar component
valery-chumak Feb 19, 2024
a9daac0
LIME-69: * add userId to payload
valery-chumak Feb 19, 2024
1ce32dd
LIME-69: * edit username
valery-chumak Feb 20, 2024
70d3e64
LIME-69: + backend logic for userUpdate
valery-chumak Feb 21, 2024
76f0d87
LIME-69: * edit request
valery-chumak Feb 21, 2024
a31057a
LIME-69: * edit validation and styles
valery-chumak Feb 21, 2024
d494db6
LIME-69: + crop spaces and leave digits
valery-chumak Feb 21, 2024
d614b9e
LIME-69: * edit user details type
valery-chumak Feb 21, 2024
fa09227
LIME-69: + token check
valery-chumak Feb 22, 2024
271bb57
LIME-69: * edit radioCard component
valery-chumak Feb 22, 2024
94eed62
LIME-69: * edit default values
valery-chumak Feb 22, 2024
f93c242
LIME-69: * button styles
valery-chumak Feb 22, 2024
106bf64
LIME-69: * solve error with label eslint
valery-chumak Feb 22, 2024
957f051
LIME-69: * fix error messages
valery-chumak Feb 22, 2024
b3b8845
LIME-69: + solve merge conflicts
valery-chumak Feb 22, 2024
0b30a31
LIME-69: * fix eslint error
valery-chumak Feb 22, 2024
19038b8
LIME-69: * edit radio component and styles
valery-chumak Feb 22, 2024
4ad70ea
LIME-69: * solve merge conflicts
valery-chumak Feb 23, 2024
b60ace7
LIME-69: * solve merge conflicts
valery-chumak Feb 23, 2024
e529c9a
LIME-69: * solve package errors
valery-chumak Feb 23, 2024
54420f6
LIME-69: * edit validation mode
valery-chumak Feb 23, 2024
84b73e3
LIME-69: + responsive design
valery-chumak Feb 24, 2024
fd0d8b8
LIME-69: * edit update service type
valery-chumak Feb 26, 2024
b686c35
LIME-69: * edit radio type
valery-chumak Feb 26, 2024
63dfd78
LIME-69: + solve merge conflicts
valery-chumak Feb 26, 2024
3f2c8e9
LIME-69: - should update func
valery-chumak Feb 26, 2024
5d72fc5
LIME-69: * changed service type
valery-chumak Feb 27, 2024
b5ea42d
LIME-69: * solve merge conflicts
valery-chumak Feb 27, 2024
c057363
LIME-69: * edit routing
valery-chumak Feb 27, 2024
82ae302
LIME-69: * fix tailwind config
valery-chumak Feb 27, 2024
f01a6fa
Merge branch 'development' into task/LIME-69-add-profile-settings
valery-chumak Feb 27, 2024
6ff19d5
LIME-69: * solve merge conflicts
valery-chumak Feb 27, 2024
6fc4c82
LIME-69: - userId as param
valery-chumak Feb 28, 2024
00c3120
LIME-69: * edit weight/height type
valery-chumak Feb 28, 2024
adaf24d
LIME-69: * change dateOfBirth handling
valery-chumak Feb 29, 2024
e074b30
LIME-69: * change migrations type of dateOfBirth
valery-chumak Feb 29, 2024
6c9f823
LIME-69: * solve merge conflicts
valery-chumak Feb 29, 2024
4be7f12
LIME-69: * fix eslint issues
valery-chumak Feb 29, 2024
8c2dd31
LIME-69: * edit updateRequest
valery-chumak Feb 29, 2024
6c0e7ec
LIME-69: * fix data handling
valery-chumak Mar 1, 2024
6abee47
LIME-69: * change to updateUserProfile
valery-chumak Mar 1, 2024
cd3b767
LIME-69: * change updateUserProfile payload
valery-chumak Mar 1, 2024
f82a456
LIME-69: * add user data as default payload
valery-chumak Mar 1, 2024
03c78ee
LIME-69: * change dateOfBirth to date
valery-chumak Mar 1, 2024
27fce08
LIME-69: * edit setDefaultValues
valery-chumak Mar 1, 2024
0162c32
LIME-69: * edit setDefaultValues
valery-chumak Mar 1, 2024
b13522b
LIME-69: * fix user repo
valery-chumak Mar 3, 2024
8c97740
LIME-69: * fix input warnings
valery-chumak Mar 3, 2024
c5bb5ef
LIME-69: * add fixes with form and validation
valery-chumak Mar 4, 2024
c42c044
LIME-69: * change range for weight/height
valery-chumak Mar 4, 2024
e66c71e
LIME-69: * fix some issues
valery-chumak Mar 4, 2024
09edfd6
LIME-69: * changed validation messages
valery-chumak Mar 4, 2024
b4c7c0e
LIME-69: * solve merge conflicts
valery-chumak Mar 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/src/bundles/users/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export { UserAttributes } from './user-attributes.enum.js';
export { UserDetailsAttributes } from './user-details-attributes.enum.js';
export { Gender, UsersApiPath } from 'shared';
export {
AuthApiPath,
HttpCode,
HttpError,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have HttpCode, HttpError in backend/src/common/http/http.ts, I guess you don't need this export here

UserValidationMessage,
} from 'shared';
1 change: 1 addition & 0 deletions backend/src/bundles/users/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export {
type UserAuthRequestDto,
type UserAuthResponseDto,
type UserGetAllResponseDto,
type UserUpdateProfileRequestDto,
type ValueOf,
} from 'shared';
2 changes: 2 additions & 0 deletions backend/src/bundles/users/user-details.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class UserDetailsModel extends AbstractModel {
public static override get tableName(): string {
return DatabaseTableName.USER_DETAILS;
}

[key: string]: unknown;
valery-chumak marked this conversation as resolved.
Show resolved Hide resolved
}

export { UserDetailsModel };
72 changes: 69 additions & 3 deletions backend/src/bundles/users/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { UserValidationMessage } from 'shared';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import


import { type UserService } from '~/bundles/users/user.service.js';
import { type UserAuthResponseDto } from '~/bundles/users/users.js';
import {
type UserAuthResponseDto,
type UserUpdateProfileRequestDto,
} from '~/bundles/users/users.js';
import {
type ApiHandlerOptions,
type ApiHandlerResponse,
BaseController,
} from '~/common/controller/controller.js';
import { BaseController } from '~/common/controller/controller.js';
import { ApiPath } from '~/common/enums/enums.js';
import { HttpCode } from '~/common/http/http.js';
import { HttpCode, HttpError } from '~/common/http/http.js';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like here

import { type Logger } from '~/common/logger/logger.js';

import { UsersApiPath } from './enums/enums.js';
Expand Down Expand Up @@ -95,6 +100,20 @@ class UserController extends BaseController {
}>,
),
});

this.addRoute({
path: `${UsersApiPath.UPDATE_USER}/:userId`,
valery-chumak marked this conversation as resolved.
Show resolved Hide resolved
method: 'PATCH',
isProtected: true,
handler: (options) =>
this.updateUser(
options as ApiHandlerOptions<{
user: UserAuthResponseDto;
body: UserUpdateProfileRequestDto;
params: { userId: string };
}>,
),
});
}

/**
Expand Down Expand Up @@ -167,6 +186,53 @@ class UserController extends BaseController {
payload: user,
};
}

private async updateUser(
options: ApiHandlerOptions<{
user: UserAuthResponseDto;
body: UserUpdateProfileRequestDto;
params: { userId: string };
}>,
): Promise<ApiHandlerResponse> {
const { user, body, params } = options;
const userId = params.userId;

try {
if (Number(userId) !== Number(user.id)) {
throw new Error('Token mismatch');
}
const updatedUser = await this.userService.update(
Number(userId),
body,
);
if (updatedUser && body.dateOfBirth) {
const [day, month, year] = body.dateOfBirth.split('/');
const parsedDate = new Date(`${year}-${month}-${day}`);

if (Number.isNaN(parsedDate.getTime())) {
throw new HttpError({
message: UserValidationMessage.BIRTHDATE_FORMAT,
status: HttpCode.BAD_REQUEST,
});
} else {
const formattedDate =
parsedDate.toLocaleDateString('en-GB');

updatedUser.dateOfBirth = formattedDate;
}
}
valery-chumak marked this conversation as resolved.
Show resolved Hide resolved

return {
status: HttpCode.OK,
payload: updatedUser,
};
} catch (error) {
throw new HttpError({
message: `Something went wrong ${error}`,
status: HttpCode.BAD_REQUEST,
});
}
}
}

export { UserController };
42 changes: 40 additions & 2 deletions backend/src/bundles/users/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { UserEntity } from '~/bundles/users/user.entity.js';
import { type UserModel } from '~/bundles/users/user.model.js';
import { type Repository } from '~/common/types/types.js';

import { HttpCode, HttpError, UserValidationMessage } from './enums/enums.js';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import HttpCode, HttpError from common

import { type UserDetailsModel } from './user-details.model.js';

class UserRepository implements Repository {
private userModel: typeof UserModel;

Expand Down Expand Up @@ -96,8 +99,43 @@ class UserRepository implements Repository {
}
}

public update(): ReturnType<Repository['update']> {
return Promise.resolve(null);
public async update(
userId: number,
updatedUserDetails: Partial<UserDetailsModel>,
): Promise<UserEntity> {
const trx = await this.userModel.startTransaction();

try {
const user = await this.userModel.query(trx).findById(userId);

if (!user) {
throw new HttpError({
message: UserValidationMessage.USER_NOT_FOUND,
status: HttpCode.NOT_FOUND,
});
}
valery-chumak marked this conversation as resolved.
Show resolved Hide resolved
await user
.$relatedQuery('userDetails', trx)
.patch(updatedUserDetails);

const userDetails = await user.$relatedQuery('userDetails', trx);

await trx.commit();

return UserEntity.initialize({
...user,
fullName: userDetails.fullName,
avatarUrl: userDetails.avatarUrl,
username: userDetails.username,
dateOfBirth: userDetails.dateOfBirth,
weight: userDetails.weight,
height: userDetails.height,
gender: userDetails.gender,
});
} catch (error) {
await trx.rollback();
throw new Error(`Error updating user details: ${error}`);
}
}

public delete(): ReturnType<Repository['delete']> {
Expand Down
48 changes: 45 additions & 3 deletions backend/src/bundles/users/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { type UserRepository } from '~/bundles/users/user.repository.js';
import { cryptService } from '~/common/services/services.js';
import { type Service } from '~/common/types/types.js';

import { HttpCode, HttpError, UserValidationMessage } from './enums/enums.js';
import {
type UserAuthRequestDto,
type UserAuthResponseDto,
type UserGetAllResponseDto,
type UserUpdateProfileRequestDto,
} from './types/types.js';
import { type UserDetailsModel } from './user-details.model.js';

class UserService implements Service {
private userRepository: UserRepository;
Expand Down Expand Up @@ -46,10 +49,49 @@ class UserService implements Service {
return user.toObject() as UserAuthResponseDto;
}

public update(): ReturnType<Service['update']> {
return Promise.resolve(null);
public async update(
userId: number,
userRequest: UserUpdateProfileRequestDto,
): Promise<UserAuthResponseDto | null> {
try {
const existingUser = await this.userRepository.find({
id: userId,
});
dimapopovych marked this conversation as resolved.
Show resolved Hide resolved
if (existingUser) {
const updatedUserDetails: Partial<UserDetailsModel> = {};
for (const property of Object.keys(userRequest)) {
const value = userRequest[property];
if (this.shouldUpdateProperty(value)) {
updatedUserDetails[property] =
property === 'weight' || property === 'height'
? Number(userRequest[property])
: userRequest[property];
}
}
const updatedUser = await this.userRepository.update(
userId,
updatedUserDetails,
);
if (!updatedUser) {
throw new HttpError({
message: UserValidationMessage.USER_NOT_FOUND,
status: HttpCode.NOT_FOUND,
});
}
return updatedUser.toObject() as UserAuthResponseDto;
} else {
throw new HttpError({
message: UserValidationMessage.USER_NOT_FOUND,
status: HttpCode.NOT_FOUND,
});
}
} catch (error) {
throw new Error(`Error occured ${error}`);
}
}
private shouldUpdateProperty(value: unknown): boolean {
return value !== null && value !== '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return value !== null && value !== '';
return value;

will this work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes) But there is an error that type 'unknown' is not assignable to type 'boolean'. So, I can make it like this return Boolean(value)

}

public delete(): ReturnType<Service['delete']> {
return Promise.resolve(true);
}
Expand Down
6 changes: 5 additions & 1 deletion backend/src/bundles/users/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ export { userController, userService };
export {
type UserAuthRequestDto,
type UserAuthResponseDto,
type UserUpdateProfileRequestDto,
} from './types/types.js';
export { UserEntity } from './user.entity.js';
export { UserModel } from './user.model.js';
export { UserService } from './user.service.js';
export { userAuthValidationSchema } from './validation-schemas/validation-schemas.js';
export {
userAuthValidationSchema,
userUpdateProfileValidationSchema,
} from './validation-schemas/validation-schemas.js';
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { userAuthValidationSchema } from 'shared';
export {
userAuthValidationSchema,
userUpdateProfileValidationSchema,
} from 'shared';
2 changes: 1 addition & 1 deletion backend/src/common/types/repository.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ type Repository<T = unknown> = {
find(query: Record<string, T>): Promise<T>;
findAll(): Promise<T[]>;
create(payload: unknown): Promise<T>;
update(): Promise<T>;
update(id: number, payload: object): Promise<T>;
delete(): Promise<boolean>;
};

Expand Down
7 changes: 6 additions & 1 deletion backend/src/common/types/service.type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { type UserUpdateProfileRequestDto } from 'shared';

type Service<T = unknown> = {
find(query: Record<string, T>): Promise<T>;
findAll(): Promise<{
items: T[];
}>;
create(payload: unknown): Promise<T>;
update(): Promise<T>;
update(
id: number,
updatedDetails: UserUpdateProfileRequestDto,
Copy link
Collaborator

@jhladkov jhladkov Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't set UserUpdateProfileRequestDto in global service type because we implement Service in many places. For example, for goals or workouts we need to update some data and we will get an error because in update we have to pass UserUpdateProfileRequestDto.

): Promise<T | null>;
delete(): Promise<boolean>;
};

Expand Down
1 change: 1 addition & 0 deletions backend/src/common/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
type ServerCommonErrorResponse,
type ServerErrorResponse,
type ServerValidationErrorResponse,
type UserUpdateProfileRequestDto,
type ValidationSchema,
type ValueOf,
} from 'shared';
3 changes: 3 additions & 0 deletions frontend/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const App: React.FC = () => {
<li>
<Link to={AppRoute.SIGN_UP}>Sign up</Link>
</li>
<li>
<Link to={AppRoute.PROFILE}>Profile</Link>
</li>
</ul>
<p>Current path: {pathname}</p>

Expand Down
5 changes: 4 additions & 1 deletion frontend/src/bundles/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ const authApi = new AuthApi({
});

export { authApi };
export { type AuthResponseDto } from './types/types.js';
export {
type AuthResponseDto,
type UserAuthResponseDto,
} from './types/types.js';
18 changes: 16 additions & 2 deletions frontend/src/bundles/auth/store/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
type AsyncThunkConfig,
type UserAuthResponseDto,
} from '~/bundles/common/types/types.js';
import { type UserAuthRequestDto } from '~/bundles/users/users.js';
import {
type UserAuthRequestDto,
type UserUpdateProfileRequestDto,
} from '~/bundles/users/users.js';
import { storage, StorageKey } from '~/framework/storage/storage.js';

import { type AuthResponseDto } from '../auth.js';
Expand Down Expand Up @@ -46,4 +49,15 @@ const refreshUser = createAsyncThunk<
return userApi.refreshUser();
});

export { refreshUser, signIn, signUp };
const updateUser = createAsyncThunk<
UserAuthResponseDto,
UserUpdateProfileRequestDto,
AsyncThunkConfig
>(`${sliceName}/update-user`, async (updateUserPayload, { extra }) => {
const { userApi } = extra;
const { id: userId } = updateUserPayload;
const userIdAsString: string = userId ? userId.toString() : '';
return await userApi.updateUser(userIdAsString, updateUserPayload);
});

export { refreshUser, signIn, signUp, updateUser };
3 changes: 2 additions & 1 deletion frontend/src/bundles/auth/store/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { refreshUser, signIn, signUp } from './actions.js';
import { refreshUser, signIn, signUp, updateUser } from './actions.js';
import { actions } from './slice.js';

const allActions = {
...actions,
signUp,
signIn,
updateUser,
refreshUser,
};

Expand Down
12 changes: 11 additions & 1 deletion frontend/src/bundles/auth/store/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type ValueOf,
} from '~/bundles/common/types/types.js';

import { refreshUser, signIn, signUp } from './actions.js';
import { refreshUser, signIn, signUp, updateUser } from './actions.js';

type State = {
dataStatus: ValueOf<typeof DataStatus>;
Expand Down Expand Up @@ -58,6 +58,16 @@ const { reducer, actions, name } = createSlice({
state.dataStatus = DataStatus.REJECTED;
state.isRefreshing = false;
});
builder.addCase(updateUser.pending, (state) => {
state.dataStatus = DataStatus.PENDING;
});
builder.addCase(updateUser.fulfilled, (state, action) => {
state.dataStatus = DataStatus.FULFILLED;
state.user = action.payload;
});
builder.addCase(updateUser.rejected, (state) => {
state.dataStatus = DataStatus.REJECTED;
});
},
});

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/bundles/auth/types/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { type AuthResponseDto } from 'shared';
export { type AuthResponseDto, type UserAuthResponseDto } from 'shared';
Loading