From 441f7b3ef247256e3e9e788f08cab61680a3125e Mon Sep 17 00:00:00 2001 From: Ivan Sai Date: Fri, 29 Dec 2023 20:41:10 +0200 Subject: [PATCH 01/67] feat: create friendship system --- server/src/app.module.ts | 2 + server/src/modules/auth/base/auth.service.ts | 15 +- .../friendship/dto/query-friends.dto.ts | 56 +++ .../friendship/dto/update-status.dto.ts | 10 + .../friendship/entities/friendship.entity.ts | 35 ++ .../friendship/friendship.controller.ts | 82 ++++ .../modules/friendship/friendship.module.ts | 16 + .../modules/friendship/friendship.service.ts | 461 ++++++++++++++++++ .../friendship/types/friendship.types.ts | 5 + .../dto/create-notification.dto.ts | 51 +- .../dto/update-friend-request-status.dto.ts | 14 + .../notifications/notifications.controller.ts | 20 +- .../notifications/notifications.service.ts | 126 ++++- .../notifications/types/notification.type.ts | 35 +- .../src/modules/users/entities/user.entity.ts | 8 +- server/src/modules/users/users.service.ts | 85 ++++ 16 files changed, 964 insertions(+), 57 deletions(-) create mode 100644 server/src/modules/friendship/dto/query-friends.dto.ts create mode 100644 server/src/modules/friendship/dto/update-status.dto.ts create mode 100644 server/src/modules/friendship/entities/friendship.entity.ts create mode 100644 server/src/modules/friendship/friendship.controller.ts create mode 100644 server/src/modules/friendship/friendship.module.ts create mode 100644 server/src/modules/friendship/friendship.service.ts create mode 100644 server/src/modules/friendship/types/friendship.types.ts create mode 100644 server/src/modules/notifications/dto/update-friend-request-status.dto.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index f56b5ba3..cd11b91e 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -24,6 +24,7 @@ import { MailerModule } from './libs/mailer/mailer.module'; import { AuthGithubModule } from './modules/auth/auth-github/auth-github.module'; import githubConfig from './config/github.config'; import { NotificationsModule } from './modules/notifications/notifications.module'; +import { FriendshipModule } from './modules/friendship/friendship.module'; @Module({ imports: [ @@ -79,6 +80,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul HomeModule, AuthGithubModule, NotificationsModule, + FriendshipModule, ], }) export class AppModule {} diff --git a/server/src/modules/auth/base/auth.service.ts b/server/src/modules/auth/base/auth.service.ts index a0b66950..b9f3b486 100644 --- a/server/src/modules/auth/base/auth.service.ts +++ b/server/src/modules/auth/base/auth.service.ts @@ -488,12 +488,15 @@ export class AuthService { } private async sendWelcomeNotification(user: User) { - await this.notificationsService.createNotification({ - receiver: user.id, - type: 'system', - data: { - system_message: 'Welcome to Teameights!', + await this.notificationsService.createNotification( + { + receiver: user.id, + type: 'system', + data: { + system_message: 'Welcome to Teameights!', + }, }, - }); + user.id + ); } } diff --git a/server/src/modules/friendship/dto/query-friends.dto.ts b/server/src/modules/friendship/dto/query-friends.dto.ts new file mode 100644 index 00000000..ab6931a2 --- /dev/null +++ b/server/src/modules/friendship/dto/query-friends.dto.ts @@ -0,0 +1,56 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { plainToInstance, Transform, Type } from 'class-transformer'; +import { Friendship } from '../entities/friendship.entity'; +export class SortFriendshipDto { + @ApiProperty() + @IsString() + orderBy: keyof Friendship; + + @ApiProperty() + @IsString() + order: string; +} + +export class FilterFriendshipDto { + @ApiProperty() + @IsOptional() + @IsNotEmpty() + status?: string; +} + +export class QueryFriendshipDto { + @ApiProperty({ + required: false, + }) + @Transform(({ value }) => (value ? Number(value) : 1)) + @IsNumber() + @IsOptional() + page: number; + + @ApiProperty({ + required: false, + }) + @Transform(({ value }) => (value ? Number(value) : 10)) + @IsNumber() + @IsOptional() + limit: number; + + @ApiProperty({ type: String, required: false }) + @IsOptional() + @Transform(({ value }) => + value ? plainToInstance(FilterFriendshipDto, JSON.parse(value)) : undefined + ) + @ValidateNested() + @Type(() => FilterFriendshipDto) + filters: FilterFriendshipDto; + + @ApiProperty({ type: String, required: false }) + @IsOptional() + @Transform(({ value }) => { + return value ? plainToInstance(SortFriendshipDto, JSON.parse(value)) : undefined; + }) + @ValidateNested({ each: true }) + @Type(() => SortFriendshipDto) + sort?: SortFriendshipDto[] | null; +} diff --git a/server/src/modules/friendship/dto/update-status.dto.ts b/server/src/modules/friendship/dto/update-status.dto.ts new file mode 100644 index 00000000..e1fe2d4d --- /dev/null +++ b/server/src/modules/friendship/dto/update-status.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsNotEmpty, IsString } from 'class-validator'; + +export class UpdateStatusDto { + @ApiProperty({ example: 'accepted' }) + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + @IsString() + @IsIn(['accepted', 'rejected'], { message: 'mustBeValidType' }) + status: 'accepted' | 'rejected'; +} diff --git a/server/src/modules/friendship/entities/friendship.entity.ts b/server/src/modules/friendship/entities/friendship.entity.ts new file mode 100644 index 00000000..50f9b8d1 --- /dev/null +++ b/server/src/modules/friendship/entities/friendship.entity.ts @@ -0,0 +1,35 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { User } from '../../users/entities/user.entity'; +import { FriendshipStatusTypes } from '../types/friendship.types'; + +@Entity() +export class Friendship { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => User, user => user.sentFriendshipRequests, { eager: true }) + creator: User; + + @ManyToOne(() => User, user => user.receivedFriendshipRequests, { eager: true }) + receiver: User; + + @Column({ type: 'enum', default: FriendshipStatusTypes.pending, enum: FriendshipStatusTypes }) + status: FriendshipStatusTypes; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date; +} diff --git a/server/src/modules/friendship/friendship.controller.ts b/server/src/modules/friendship/friendship.controller.ts new file mode 100644 index 00000000..21bf1b45 --- /dev/null +++ b/server/src/modules/friendship/friendship.controller.ts @@ -0,0 +1,82 @@ +import { + Controller, + Post, + Patch, + Param, + UseGuards, + HttpCode, + HttpStatus, + Body, + Request, + Delete, + Get, + Query, +} from '@nestjs/common'; +import { FriendshipService } from './friendship.service'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { UpdateStatusDto } from './dto/update-status.dto'; +import { infinityPagination } from '../../utils/infinity-pagination'; +import { QueryFriendshipDto } from './dto/query-friends.dto'; + +@ApiTags('Friendship') +@Controller({ + path: 'friendship', + version: '1', +}) +@Controller('friendship') +export class FriendshipController { + constructor(private readonly friendshipService: FriendshipService) {} + + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @Post('/:receiverId') + @HttpCode(HttpStatus.CREATED) + async create(@Param('receiverId') receiverId: number, @Request() req) { + await this.friendshipService.createFriendship(req.user.id, receiverId); + } + + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @HttpCode(HttpStatus.OK) + @Patch('/:creatorId') + async updateStatus( + @Param('creatorId') creatorId: number, + @Body() dto: UpdateStatusDto, + @Request() req + ) { + await this.friendshipService.updateStatus(creatorId, req.user.id, dto); + } + + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @HttpCode(HttpStatus.NO_CONTENT) + @Delete('/:friendId') + async deleteRequestOrFriendship(@Param('friendId') friendId: number, @Request() req) { + await this.friendshipService.deleteRequestOrFriendship(friendId, req.user.id); + } + + @HttpCode(HttpStatus.OK) + @Get('/:userId') + async findAll(@Param('userId') userId: number, @Query() query: QueryFriendshipDto) { + const page = query?.page ?? 1; + let limit = query?.limit ?? 50; + + if (limit > 50) { + limit = 50; + } + + return infinityPagination( + await this.friendshipService.findManyWithPagination({ + userId: userId, + filterOptions: query?.filters, + sortOptions: query?.sort, + paginationOptions: { + page, + limit, + }, + }), + { page, limit } + ); + } +} diff --git a/server/src/modules/friendship/friendship.module.ts b/server/src/modules/friendship/friendship.module.ts new file mode 100644 index 00000000..769e3666 --- /dev/null +++ b/server/src/modules/friendship/friendship.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { FriendshipService } from './friendship.service'; +import { FriendshipController } from './friendship.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Friendship } from './entities/friendship.entity'; +import { NotificationsService } from '../notifications/notifications.service'; +import { Notification } from '../notifications/entities/notification.entity'; +import { UsersService } from '../users/users.service'; +import { User } from '../users/entities/user.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Friendship, Notification, User])], + controllers: [FriendshipController], + providers: [FriendshipService, NotificationsService, UsersService], +}) +export class FriendshipModule {} diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts new file mode 100644 index 00000000..f88dc026 --- /dev/null +++ b/server/src/modules/friendship/friendship.service.ts @@ -0,0 +1,461 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FindOptionsWhere, Repository } from 'typeorm'; +import { Friendship } from './entities/friendship.entity'; +import { NotificationsService } from '../notifications/notifications.service'; +import { UsersService } from '../users/users.service'; +import { EntityCondition } from '../../utils/types/entity-condition.type'; +import { NullableType } from '../../utils/types/nullable.type'; +import { FriendshipStatusTypes } from './types/friendship.types'; +import { UpdateStatusDto } from './dto/update-status.dto'; +import { + FriendRequestNotificationData, + NotificationStatusEnum, +} from '../notifications/types/notification.type'; + +import { IPaginationOptions } from '../../utils/types/pagination-options'; + +import { FilterFriendshipDto, SortFriendshipDto } from './dto/query-friends.dto'; + +@Injectable() +export class FriendshipService { + constructor( + @InjectRepository(Friendship) + private friendshipRepository: Repository, + private notificationsService: NotificationsService, + private usersService: UsersService + ) {} + + static BAN_TIME_IN_MONTHS = 1; + + public async findOne(fields: EntityCondition): Promise> { + return await this.friendshipRepository.findOne({ + where: fields, + }); + } + + async createFriendship(creatorId: number, receiverId: number) { + if (creatorId === receiverId) { + throw new HttpException( + { + status: HttpStatus.BAD_REQUEST, + errors: { + message: 'You can`t add yourself as a friend', + }, + }, + HttpStatus.BAD_REQUEST + ); + } + const creator = await this.usersService.findOne({ id: creatorId }); + + if (!creator) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${receiverId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + const receiver = await this.usersService.findOne({ id: receiverId }); + + if (!receiver) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${receiverId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + await this.checkIfRequestAllowed(creatorId, receiverId); + + //Check if was rejected and then check rejection date + if ( + await this.friendshipRepository.exist({ + where: { + creator: { id: creatorId }, + receiver: { id: receiverId }, + status: FriendshipStatusTypes['rejected'], + }, + }) + ) { + const friendship = await this.findOne({ + creator: { id: creatorId }, + receiver: { id: receiverId }, + status: FriendshipStatusTypes['rejected'], + }); + if (friendship) { + const banEndTime = new Date(friendship.updatedAt); + banEndTime.setMonth(banEndTime.getMonth() + FriendshipService.BAN_TIME_IN_MONTHS); + if (new Date() < banEndTime) { + throw new HttpException( + { + status: HttpStatus.CONFLICT, + errors: { + friendship: `users with id: ${creatorId} cant send request for user with id: ${receiverId} right now`, + }, + }, + HttpStatus.CONFLICT + ); + } else { + friendship.status = FriendshipStatusTypes.pending; + await this.notificationsService.createNotification( + { + receiver: receiverId, + type: 'friend_request', + }, + creatorId + ); + await this.friendshipRepository.save(friendship); + return; + } + } + } + + await this.notificationsService.createNotification( + { + receiver: receiverId, + type: 'friend_request', + }, + creatorId + ); + await this.friendshipRepository.save( + this.friendshipRepository.create({ + creator: creator, + receiver: receiver, + status: FriendshipStatusTypes.pending, + }) + ); + } + + private async checkIfRequestAllowed(creatorId: number, receiverId: number) { + //Check if users already friends + if ( + (await this.friendshipRepository.exist({ + where: { + creator: { id: creatorId }, + receiver: { id: receiverId }, + status: FriendshipStatusTypes['accepted'], + }, + })) || + (await this.friendshipRepository.exist({ + where: { + creator: { id: receiverId }, + receiver: { id: creatorId }, + status: FriendshipStatusTypes['accepted'], + }, + })) + ) { + throw new HttpException( + { + status: HttpStatus.CONFLICT, + errors: { + friendship: `users already have friendship`, + }, + }, + HttpStatus.CONFLICT + ); + } + + //Check if request already sent + if ( + await this.friendshipRepository.exist({ + where: { + creator: { id: creatorId }, + receiver: { id: receiverId }, + status: FriendshipStatusTypes['pending'], + }, + }) + ) { + throw new HttpException( + { + status: HttpStatus.CONFLICT, + errors: { + friendship: `users with id: ${creatorId} already have sent request for user with id: ${receiverId}`, + }, + }, + HttpStatus.CONFLICT + ); + } + + //Check if creator have pending request from receiver + if ( + await this.friendshipRepository.exist({ + where: { + creator: { id: receiverId }, + receiver: { id: creatorId }, + status: FriendshipStatusTypes['pending'], + }, + }) + ) { + throw new HttpException( + { + status: HttpStatus.CONFLICT, + errors: { + friendship: `users with id: ${creatorId} already have pending request from user with id: ${receiverId}`, + }, + }, + HttpStatus.CONFLICT + ); + } + } + + async updateStatus(creatorId: number, receiverId: number, dto: UpdateStatusDto) { + if (creatorId === receiverId) { + throw new HttpException( + { + status: HttpStatus.BAD_REQUEST, + errors: { + message: 'You can`t have request to yourself', + }, + }, + HttpStatus.BAD_REQUEST + ); + } + const creator = await this.usersService.findOne({ id: creatorId }); + + if (!creator) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${receiverId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + const receiver = await this.usersService.findOne({ id: receiverId }); + + if (!receiver) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${receiverId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + const friendship = await this.findOne({ + creator: { id: creatorId }, + receiver: { id: receiverId }, + status: FriendshipStatusTypes.pending, + }); + if (!friendship) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + friendship: `user with id: ${receiverId} dont have pending request from user with id: ${creatorId}`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + const notification = await this.notificationsService.findOne({ + receiver: { id: receiverId }, + data: { status: NotificationStatusEnum.pending, creator: creator.toJSON() }, + }); + if (!notification) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + notification: `notification not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + (notification.data as FriendRequestNotificationData).status = + NotificationStatusEnum[dto.status]; + await this.notificationsService.save(notification); + + friendship.status = FriendshipStatusTypes[dto.status]; + await this.friendshipRepository.save(friendship); + if (dto.status === 'accepted') { + await this.notificationsService.createNotification( + { + receiver: creatorId, + type: 'system', + data: { + system_message: `${receiver.username} accepted your friend request!`, + }, + }, + receiverId + ); + } + } + + async deleteRequestOrFriendship(friendId: number, userId: number) { + if (userId === friendId) { + throw new HttpException( + { + status: HttpStatus.BAD_REQUEST, + errors: { + message: 'You can`t do this operation with yourself', + }, + }, + HttpStatus.BAD_REQUEST + ); + } + const user = await this.usersService.findOne({ id: userId }); + + if (!user) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${user} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + const friend = await this.usersService.findOne({ id: friendId }); + + if (!friend) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${friendId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + //Friends case + if ( + (await this.friendshipRepository.exist({ + where: { + creator: { id: userId }, + receiver: { id: friendId }, + status: FriendshipStatusTypes['accepted'], + }, + })) || + (await this.friendshipRepository.exist({ + where: { + creator: { id: friendId }, + receiver: { id: userId }, + status: FriendshipStatusTypes['accepted'], + }, + })) + ) { + const friendshipsToDelete = await this.friendshipRepository.find({ + where: [ + { + creator: { id: userId }, + receiver: { id: friendId }, + status: FriendshipStatusTypes.accepted, + }, + { + creator: { id: friendId }, + receiver: { id: userId }, + status: FriendshipStatusTypes.accepted, + }, + ], + }); + + await this.friendshipRepository.remove(friendshipsToDelete); + return; + } + + if ( + await this.friendshipRepository.exist({ + where: { + creator: { id: userId }, + receiver: { id: friendId }, + status: FriendshipStatusTypes['pending'], + }, + }) + ) { + const friendship = await this.findOne({ + creator: { id: userId }, + receiver: { id: friendId }, + status: FriendshipStatusTypes['pending'], + }); + if (friendship) { + friendship.status = FriendshipStatusTypes.rejected; + await this.friendshipRepository.save(friendship); + const notification = await this.notificationsService.findOne({ + receiver: { id: friendId }, + data: { status: NotificationStatusEnum.pending, creator: user.toJSON() }, + }); + if (notification) { + await this.notificationsService.deleteNotification(notification); + } + } + } else { + throw new HttpException( + { + status: HttpStatus.BAD_REQUEST, + errors: { + message: 'You can`t do this operation', + }, + }, + HttpStatus.BAD_REQUEST + ); + } + } + + async findManyWithPagination({ + userId, + filterOptions, + sortOptions, + paginationOptions, + }: { + userId: number; + filterOptions: FilterFriendshipDto; + sortOptions?: SortFriendshipDto[] | null; + paginationOptions: IPaginationOptions; + }): Promise { + const where: FindOptionsWhere[] = [ + { receiver: { id: userId } }, + { creator: { id: userId } }, // Отдельное условие для поиска по creatorId + ]; + + if (filterOptions?.status) { + switch (filterOptions.status) { + case FriendshipStatusTypes.accepted: + where.forEach(condition => (condition.status = FriendshipStatusTypes.accepted)); + break; + case FriendshipStatusTypes.rejected: + where.forEach(condition => (condition.status = FriendshipStatusTypes.rejected)); + break; + case FriendshipStatusTypes.pending: + where.forEach(condition => (condition.status = FriendshipStatusTypes.pending)); + break; + default: + // Handle the default case or leave it empty if not needed + break; + } + } + + return this.friendshipRepository.find({ + skip: (paginationOptions.page - 1) * paginationOptions.limit, + take: paginationOptions.limit, + where: where, + order: sortOptions?.reduce( + (accumulator, sort) => ({ + ...accumulator, + [sort.orderBy]: sort.order, + }), + {} + ), + }); + } +} diff --git a/server/src/modules/friendship/types/friendship.types.ts b/server/src/modules/friendship/types/friendship.types.ts new file mode 100644 index 00000000..d89c9347 --- /dev/null +++ b/server/src/modules/friendship/types/friendship.types.ts @@ -0,0 +1,5 @@ +export enum FriendshipStatusTypes { + accepted = 'accepted', + pending = 'pending', + rejected = 'rejected', +} diff --git a/server/src/modules/notifications/dto/create-notification.dto.ts b/server/src/modules/notifications/dto/create-notification.dto.ts index 88bc110f..6cb34272 100644 --- a/server/src/modules/notifications/dto/create-notification.dto.ts +++ b/server/src/modules/notifications/dto/create-notification.dto.ts @@ -1,5 +1,13 @@ import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { IsIn, IsNotEmpty, IsNumber, IsObject, IsString, ValidateNested } from 'class-validator'; +import { + IsIn, + IsNotEmpty, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import { Transform, Type } from 'class-transformer'; export class SystemNotificationDataDto { @@ -9,22 +17,6 @@ export class SystemNotificationDataDto { system_message: string; } -// export class TeamInvNotificationDataDto { -// @ApiProperty({ example: 'nmashchenko' }) -// @Transform(lowerCaseTransformer) -// @IsNotEmpty() -// @IsOptional() -// from_user: string; -// -// @IsString() -// @IsNotEmpty() -// teamTo: string; -// -// @IsString() -// @IsOptional() -// message?: string; -// } - @ApiExtraModels(SystemNotificationDataDto) export class CreateNotificationDto { @ApiProperty({ example: '1' }) @@ -32,15 +24,32 @@ export class CreateNotificationDto { @IsNumber() receiver: number; - @ApiProperty({ enum: ['system', 'team_invitation'] }) + @ApiProperty({ enum: ['system', 'friend_request'] }) @IsNotEmpty({ message: 'mustBeNotEmpty' }) - @IsIn(['system', 'team_invitation'], { message: 'mustBeValidType' }) - type: 'system' | 'team_invitation'; + @IsIn(['system', 'friend_request'], { message: 'mustBeValidType' }) + type: 'system' | 'friend_request'; @ApiProperty() @IsNotEmpty() @IsObject({ message: 'Should be object' }) @ValidateNested() @Type(() => SystemNotificationDataDto) - data: SystemNotificationDataDto; + @IsOptional() + data?: SystemNotificationDataDto; } + +// export class TeamInvNotificationDataDto { +// @ApiProperty({ example: 'nmashchenko' }) +// @Transform(lowerCaseTransformer) +// @IsNotEmpty() +// @IsOptional() +// from_user: string; +// +// @IsString() +// @IsNotEmpty() +// teamTo: string; +// +// @IsString() +// @IsOptional() +// message?: string; +// } diff --git a/server/src/modules/notifications/dto/update-friend-request-status.dto.ts b/server/src/modules/notifications/dto/update-friend-request-status.dto.ts new file mode 100644 index 00000000..0279acda --- /dev/null +++ b/server/src/modules/notifications/dto/update-friend-request-status.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsNotEmpty, IsNumber } from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class UpdateFriendRequestStatusDto { + @ApiProperty({ example: '1' }) + @Transform(({ value }) => (value ? Number(value) : undefined)) + @IsNumber() + notificationId: number; + @ApiProperty({ enum: ['accepted', 'rejected'] }) + @IsNotEmpty({ message: 'mustBeNotEmpty' }) + @IsIn(['accepted', 'rejected'], { message: 'mustBeValidType' }) + status: 'accepted' | 'rejected'; +} diff --git a/server/src/modules/notifications/notifications.controller.ts b/server/src/modules/notifications/notifications.controller.ts index 7ae153ae..40297a26 100644 --- a/server/src/modules/notifications/notifications.controller.ts +++ b/server/src/modules/notifications/notifications.controller.ts @@ -12,6 +12,7 @@ import { SerializeOptions, Request, UseGuards, + HttpException, } from '@nestjs/common'; import { RoleEnum } from '../../libs/database/metadata/roles/roles.enum'; import { RolesGuard } from '../../libs/database/metadata/roles/roles.guard'; @@ -36,11 +37,22 @@ export class NotificationsController { @Post() @ApiBearerAuth() - @Roles(RoleEnum.admin) + @Roles(RoleEnum.admin, RoleEnum.user) @UseGuards(AuthGuard('jwt'), RolesGuard) @HttpCode(HttpStatus.CREATED) - async createNotification(@Body() dto: CreateNotificationDto) { - return await this.notificationService.createNotification(dto); + async createNotification(@Body() dto: CreateNotificationDto, @Request() request) { + if (request.user.role.name !== 'Admin' && dto.type == 'system') { + throw new HttpException( + { + status: HttpStatus.FORBIDDEN, + errors: { + message: `Forbidden resource`, + }, + }, + HttpStatus.FORBIDDEN + ); + } + return await this.notificationService.createNotification(dto, request.user.id); } @SerializeOptions({ @@ -96,6 +108,6 @@ export class NotificationsController { @UseGuards(AuthGuard('jwt')) @HttpCode(HttpStatus.NO_CONTENT) async deleteNotification(@Request() request, @Param('id') id: number) { - await this.notificationService.deleteNotification(id, request.user); + await this.notificationService.deleteNotificationByUser(id, request.user); } } diff --git a/server/src/modules/notifications/notifications.service.ts b/server/src/modules/notifications/notifications.service.ts index fb94471c..c5597997 100644 --- a/server/src/modules/notifications/notifications.service.ts +++ b/server/src/modules/notifications/notifications.service.ts @@ -4,7 +4,7 @@ import { FindOptionsWhere, Repository } from 'typeorm'; import { Notification } from './entities/notification.entity'; import { UsersService } from '../users/users.service'; import { CreateNotificationDto, SystemNotificationDataDto } from './dto/create-notification.dto'; -import { NotificationTypesEnum } from './types/notification.type'; +import { NotificationStatusEnum, NotificationTypesEnum } from './types/notification.type'; import { EntityCondition } from '../../utils/types/entity-condition.type'; import { NullableType } from '../../utils/types/nullable.type'; import { FilterNotificationDto, SortNotificationDto } from './dto/query-notification.dto'; @@ -55,7 +55,7 @@ export class NotificationsService { } } - public async deleteNotification(id: number, userJwtPayload: JwtPayloadType) { + public async deleteNotificationByUser(id: number, userJwtPayload: JwtPayloadType) { const notification = await this.findOne({ id: id }); if (!notification) { @@ -111,9 +111,9 @@ export class NotificationsService { case NotificationTypesEnum.system: where.type = NotificationTypesEnum.system; break; - case NotificationTypesEnum.team_invitation: - where.type = NotificationTypesEnum.team_invitation; - break; + // case NotificationTypesEnum.team_invitation: + // where.type = NotificationTypesEnum.team_invitation; + // break; default: // Handle the default case or leave it empty if not needed break; @@ -138,7 +138,7 @@ export class NotificationsService { }); } - async createNotification(dto: CreateNotificationDto) { + async createNotification(dto: CreateNotificationDto, requestUserId: number) { const user = await this.usersService.findOne({ id: dto.receiver }); if (!user) { @@ -153,8 +153,7 @@ export class NotificationsService { ); } - const data = this.getDataByType(dto); - + const data = await this.getDataByType(dto, requestUserId); await this.notificationRepository.save( this.notificationRepository.create({ receiver: user, @@ -164,17 +163,124 @@ export class NotificationsService { ); } - private getDataByType(dto: CreateNotificationDto) { + private async getDataByType(dto: CreateNotificationDto, requestUserId: number) { switch (dto.type) { - case 'system': + case 'system': { const data = dto.data as SystemNotificationDataDto; return { system_message: data.system_message, }; + } // Add more cases here as needed + case 'friend_request': { + if (dto.receiver == requestUserId) { + throw new HttpException( + { + status: HttpStatus.BAD_REQUEST, + errors: { + message: `You can't add yourself as a friend`, + }, + }, + HttpStatus.BAD_REQUEST + ); + } + const creator = await this.usersService.findOne({ id: requestUserId }); + if (!creator) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${requestUserId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + return { + creator: creator, + status: NotificationStatusEnum.pending, + }; + } default: // Handle the default case or leave it empty if not needed break; } } + + // async updateFriendRequestStatus( + // dto: UpdateFriendRequestStatusDto, + // userJwtPayload: JwtPayloadType + // ) { + // const notification = await this.findOne({ id: dto.notificationId }); + // if (!notification) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // notification: `notification with id: ${dto.notificationId} was not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // + // if (notification.receiver.id !== userJwtPayload.id) { + // throw new HttpException( + // { + // status: HttpStatus.UNAUTHORIZED, + // errors: { + // notification: `current user can't update this notification status. administrator was notified about this action.`, + // }, + // }, + // HttpStatus.UNAUTHORIZED + // ); + // } + // const data = notification.data as FriendRequestNotificationData; + // if (data.status !== 'pending') { + // throw new HttpException( + // { + // status: HttpStatus.FORBIDDEN, + // error: 'Friend request already accepted or rejected', + // }, + // HttpStatus.FORBIDDEN + // ); + // } + // if (dto.status === 'accepted') { + // const creator = await this.usersService.findOne({ id: data.creator.id }); + // if (!creator) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // user: `user with id: ${data.creator.id} was not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // const receiver = await this.usersService.findOne({ id: notification.receiver.id }); + // if (!receiver) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // user: `user with id: ${notification.receiver.id} was not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // + // await this.usersService.makeFriendship(creator, receiver); + // } + // data.status = NotificationStatusEnum[dto.status]; + // await this.notificationRepository.save(notification); + // } + async save(notification: Notification) { + await this.notificationRepository.save(notification); + } + + async deleteNotification(notification: Notification) { + await this.notificationRepository.remove(notification); + } } diff --git a/server/src/modules/notifications/types/notification.type.ts b/server/src/modules/notifications/types/notification.type.ts index f814ebeb..c4220da3 100644 --- a/server/src/modules/notifications/types/notification.type.ts +++ b/server/src/modules/notifications/types/notification.type.ts @@ -1,16 +1,28 @@ -import { IsNotEmpty, IsString } from 'class-validator'; +import { User } from '../../users/entities/user.entity'; + +export enum NotificationTypesEnum { + system = 'system', + // team_invitation = 'team_invitation', + friend_request = 'friend_request', +} + +export enum NotificationStatusEnum { + rejected = 'rejected', + pending = 'pending', + accepted = 'accepted', +} export class SystemNotificationData { - @IsString() - @IsNotEmpty() system_message: string; } -// export enum NotificationStatusEnum { -// pending = 0, -// accepted = 1, -// rejected = 2, -// } +export class FriendRequestNotificationData { + status: NotificationStatusEnum; + + creator: User; +} + +export type NotificationTypeData = SystemNotificationData | FriendRequestNotificationData; // export class TeamInvitationNotificationData { // @IsObject() @@ -29,10 +41,3 @@ export class SystemNotificationData { // @Column({ type: 'enum', enum: NotificationStatusEnum, default: NotificationStatusEnum.pending }) // status: NotificationStatusEnum; // } - -export type NotificationTypeData = SystemNotificationData; - -export enum NotificationTypesEnum { - system = 'system', - team_invitation = 'team_invitation', -} diff --git a/server/src/modules/users/entities/user.entity.ts b/server/src/modules/users/entities/user.entity.ts index 7b422f4e..38e5bf0f 100644 --- a/server/src/modules/users/entities/user.entity.ts +++ b/server/src/modules/users/entities/user.entity.ts @@ -27,6 +27,7 @@ import { Projects } from './projects.entity'; import { Links } from './links.entity'; import { Skills } from './skills.entity'; import { Notification } from '../../notifications/entities/notification.entity'; +import { Friendship } from '../../friendship/entities/friendship.entity'; @Entity() export class User extends EntityHelper { @@ -144,9 +145,14 @@ export class User extends EntityHelper { @OneToMany(() => Notification, notifications => notifications.receiver) notifications: Notification[]; + @OneToMany(() => Friendship, friendship => friendship.creator) + sentFriendshipRequests: Friendship[]; + + @OneToMany(() => Friendship, friendship => friendship.receiver) + receivedFriendshipRequests: Friendship[]; + // @ManyToOne(() => Team, team => team.users) // team: Team; - @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/server/src/modules/users/users.service.ts b/server/src/modules/users/users.service.ts index ab9fce31..30720ba0 100644 --- a/server/src/modules/users/users.service.ts +++ b/server/src/modules/users/users.service.ts @@ -83,4 +83,89 @@ export class UsersService { async softDelete(id: User['id']): Promise { await this.usersRepository.softDelete(id); } + + // async makeFriendship(creator: User, receiver: User) { + // if (!creator.friends) { + // creator.friends = []; + // } + // if (!receiver.friends) { + // receiver.friends = []; + // } + // + // creator.friends.push(receiver); + // receiver.friends.push(creator); + // await this.usersRepository.save(creator); + // await this.usersRepository.save(receiver); + // } + + // async findFriends(id: number) { + // const user = await this.usersRepository.findOne({ where: { id: id }, relations: ['friends'] }); + // if (!user) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // user: `user with id: ${id} was not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // return user.friends; + // } + + // async deleteFriend(id: number, friendId: number, userJwtPayload: JwtPayloadType) { + // if (id !== userJwtPayload.id) { + // throw new HttpException( + // { + // status: HttpStatus.UNAUTHORIZED, + // errors: { + // notification: `current user can't do this action. administrator was notified about this action.`, + // }, + // }, + // HttpStatus.UNAUTHORIZED + // ); + // } + // const user = await this.usersRepository.findOne({ where: { id: id }, relations: ['friends'] }); + // if (!user) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // user: `User with id: ${id} was not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // + // const friend = await this.usersRepository.findOne({ + // where: { id: friendId }, + // relations: ['friends'], + // }); + // if (!friend) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // user: `User with id: ${friendId} was not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // + // const userFriendIndex = user.friends.findIndex(userFriend => userFriend.id === friend.id); + // const friendFriendIndex = friend.friends.findIndex(friendFriend => friendFriend.id === user.id); + // + // if (userFriendIndex !== -1) { + // user.friends.splice(userFriendIndex, 1); + // await this.usersRepository.save(user); + // } + // + // if (friendFriendIndex !== -1) { + // friend.friends.splice(friendFriendIndex, 1); + // await this.usersRepository.save(friend); + // } + // } } From 292ba7a122025a3659649004dc8c21eaef8952e9 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 25 Jan 2024 20:35:48 +0200 Subject: [PATCH 02/67] test: add e2e tests for friendship functionality --- server/test/friendship/friendship.e2e-spec.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 server/test/friendship/friendship.e2e-spec.ts diff --git a/server/test/friendship/friendship.e2e-spec.ts b/server/test/friendship/friendship.e2e-spec.ts new file mode 100644 index 00000000..c44fa170 --- /dev/null +++ b/server/test/friendship/friendship.e2e-spec.ts @@ -0,0 +1,132 @@ +import request from 'supertest'; +import { APP_URL, TESTER_EMAIL, TESTER_PASSWORD } from '../utils/constants'; + +describe('Friendship (e2e)', () => { + const app = APP_URL; + const receiverEmail = 'test@example.com'; + + it('Send friendship request to existing user: /api/v1/friendship/:receiverId (POST)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .post('/api/v1/friendship/3') + .auth(userApiToken, { + type: 'bearer', + }) + .expect(201); + }); + + it('Send friendship request to existing user that you already sent request: /api/v1/friendship/:receiverId (POST)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .post('/api/v1/friendship/3') + .auth(userApiToken, { + type: 'bearer', + }) + .expect(409); + }); + + it('Create friendship with yourself: /api/v1/friendship/:receiverId (POST)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .post('/api/v1/friendship/2') + .auth(userApiToken, { + type: 'bearer', + }) + .expect(400); + }); + + it('Accept friendship request: /api/v1/friendship/:creatorId (UPDATE)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: receiverEmail, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .patch('/api/v1/friendship/2') + .auth(userApiToken, { + type: 'bearer', + }) + .send({ + status: 'accepted', + }) + .expect(200); + }); + it('Try to accept not your friendship request: /api/v1/friendship/:creatorId (UPDATE)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .patch('/api/v1/friendship/3') + .auth(userApiToken, { + type: 'bearer', + }) + .send({ + status: 'accepted', + }) + .expect(404); + }); + + it('Delete friendship : /api/v1/friendship/:friendId (DELETE)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .delete('/api/v1/friendship/3') + .auth(userApiToken, { + type: 'bearer', + }) + .expect(204); + }); + + it('Cancel friendship request: /api/v1/friendship/:friendId (DELETE)', async () => { + const creatorApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app).post('/api/v1/friendship/3').auth(creatorApiToken, { + type: 'bearer', + }); + + await request(app) + .delete('/api/v1/friendship/3') + .auth(creatorApiToken, { + type: 'bearer', + }) + .expect(204); + }); + + it('Send friendship request to existing user, but you banned for some time: /api/v1/friendship/:receiverId (POST)', async () => { + const userApiToken = await request(app) + .post('/api/v1/auth/email/login') + .send({ email: TESTER_EMAIL, password: TESTER_PASSWORD }) + .then(({ body }) => body.token); + + await request(app) + .post('/api/v1/friendship/3') + .auth(userApiToken, { + type: 'bearer', + }) + .expect(409); + }); + + it('Get friends list: /api/v1/friendship/:userId (GET)', async () => { + await request(app).get('/api/v1/friendship/2').expect(200); + }); +}); From 75853b12769b764aa47580644fa8bced89fa207d Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:11:12 +0100 Subject: [PATCH 03/67] wip: add basic profile layout --- .../src/app/(main)/profile/layout.module.scss | 9 ++++ client/src/app/(main)/profile/layout.tsx | 29 ++++++++++++ client/src/app/(main)/profile/page.tsx | 5 ++ .../app/(main)/profile/ui/about.module.scss | 3 ++ client/src/app/(main)/profile/ui/about.tsx | 22 +++++++++ .../app/(main)/profile/ui/card.module.scss | 5 ++ client/src/app/(main)/profile/ui/card.tsx | 16 +++++++ .../app/(main)/profile/ui/header.module.scss | 33 +++++++++++++ client/src/app/(main)/profile/ui/header.tsx | 46 +++++++++++++++++++ .../app/(main)/profile/ui/list.module.scss | 0 client/src/app/(main)/profile/ui/list.tsx | 19 ++++++++ .../src/app/(main)/profile/ui/row.module.scss | 0 client/src/app/(main)/profile/ui/row.tsx | 16 +++++++ 13 files changed, 203 insertions(+) create mode 100644 client/src/app/(main)/profile/layout.module.scss create mode 100644 client/src/app/(main)/profile/layout.tsx create mode 100644 client/src/app/(main)/profile/page.tsx create mode 100644 client/src/app/(main)/profile/ui/about.module.scss create mode 100644 client/src/app/(main)/profile/ui/about.tsx create mode 100644 client/src/app/(main)/profile/ui/card.module.scss create mode 100644 client/src/app/(main)/profile/ui/card.tsx create mode 100644 client/src/app/(main)/profile/ui/header.module.scss create mode 100644 client/src/app/(main)/profile/ui/header.tsx create mode 100644 client/src/app/(main)/profile/ui/list.module.scss create mode 100644 client/src/app/(main)/profile/ui/list.tsx create mode 100644 client/src/app/(main)/profile/ui/row.module.scss create mode 100644 client/src/app/(main)/profile/ui/row.tsx diff --git a/client/src/app/(main)/profile/layout.module.scss b/client/src/app/(main)/profile/layout.module.scss new file mode 100644 index 00000000..69e646a5 --- /dev/null +++ b/client/src/app/(main)/profile/layout.module.scss @@ -0,0 +1,9 @@ + +.container { + display: flex; + width: 100%; + + .body { + width: 100%; + } +} diff --git a/client/src/app/(main)/profile/layout.tsx b/client/src/app/(main)/profile/layout.tsx new file mode 100644 index 00000000..9b949473 --- /dev/null +++ b/client/src/app/(main)/profile/layout.tsx @@ -0,0 +1,29 @@ +'use client'; +import styles from './layout.module.scss'; +import { useGetMe } from '@/entities/session'; +import { Header } from './ui/header'; +import { Flex } from '@/shared/ui'; +import { List } from './ui/list'; +import { Skeleton } from '@/shared/ui/skeleton/skeleton'; +import { About } from './ui/about'; +export default function Layout() { + const { data: user } = useGetMe(); + + let body = ; + if (user) { + body = ( + + + + + ); + } + return ( +
+ +
+ {body} + +
+ ); +} diff --git a/client/src/app/(main)/profile/page.tsx b/client/src/app/(main)/profile/page.tsx new file mode 100644 index 00000000..7fcbc82f --- /dev/null +++ b/client/src/app/(main)/profile/page.tsx @@ -0,0 +1,5 @@ +'use client'; + +export default function Page() { + return <>; +} diff --git a/client/src/app/(main)/profile/ui/about.module.scss b/client/src/app/(main)/profile/ui/about.module.scss new file mode 100644 index 00000000..11be4b36 --- /dev/null +++ b/client/src/app/(main)/profile/ui/about.module.scss @@ -0,0 +1,3 @@ +.container { + +} diff --git a/client/src/app/(main)/profile/ui/about.tsx b/client/src/app/(main)/profile/ui/about.tsx new file mode 100644 index 00000000..a4c67d54 --- /dev/null +++ b/client/src/app/(main)/profile/ui/about.tsx @@ -0,0 +1,22 @@ +import { useGetMe } from '@/entities/session'; +import { Card } from './card'; +import { Flex, Typography } from '@/shared/ui'; +import { Github, Google } from '@/shared/assets'; + +export const About = () => { + const { data: user } = useGetMe(); + return ( + + + + About + + {user?.description ?? 'Description'} + + + + + + + ); +}; diff --git a/client/src/app/(main)/profile/ui/card.module.scss b/client/src/app/(main)/profile/ui/card.module.scss new file mode 100644 index 00000000..6fc7e5fc --- /dev/null +++ b/client/src/app/(main)/profile/ui/card.module.scss @@ -0,0 +1,5 @@ +.card { + background: rgba(26, 28, 34, 1); + border-radius: 15px; + padding: 32px; +} diff --git a/client/src/app/(main)/profile/ui/card.tsx b/client/src/app/(main)/profile/ui/card.tsx new file mode 100644 index 00000000..95ed2d20 --- /dev/null +++ b/client/src/app/(main)/profile/ui/card.tsx @@ -0,0 +1,16 @@ +import { type CSSProperties, ReactNode } from 'react'; +import styles from './card.module.scss'; + +interface CardProps { + children: ReactNode; + style?: CSSProperties; + borderRadius?: string; +} + +export const Card = ({ children, style }: CardProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/client/src/app/(main)/profile/ui/header.module.scss b/client/src/app/(main)/profile/ui/header.module.scss new file mode 100644 index 00000000..19cf62e6 --- /dev/null +++ b/client/src/app/(main)/profile/ui/header.module.scss @@ -0,0 +1,33 @@ +.container { + width: 100%; + overflow: hidden; + border-radius: 15px; + background: rgba(67, 71, 82); +} + +.background { + height: 130px; +} + +.header { + background: rgba(26, 28, 34, 1); + padding: 24px 32px 32px 32px; + display: flex; + align-items: end; + justify-content: space-between; + height: 118px; + + .interactable { + align-self: start; + } +} +.profile { + margin-top: -44px; + display: flex; + align-items: end; + gap: 24px; + .avatar { + .image { + } + } +} diff --git a/client/src/app/(main)/profile/ui/header.tsx b/client/src/app/(main)/profile/ui/header.tsx new file mode 100644 index 00000000..e5e8a865 --- /dev/null +++ b/client/src/app/(main)/profile/ui/header.tsx @@ -0,0 +1,46 @@ +'use client'; +import styles from './header.module.scss'; +import { useGetMe } from '@/entities/session'; +import { UserPlusIcon } from '@/shared/assets'; +import { Button, Flex, ImageLoader, Typography } from '@/shared/ui'; +import { Skeleton } from '@/shared/ui/skeleton/skeleton'; +export const Header = () => { + const { data: user } = useGetMe(); + if (!user) { + return ; + } + + const username = user.username ? '@' + user.username : ''; + return ( +
+
+
+
+
+
+ +
+ +
{user.fullName}
+ + {username} + +
+
+ +
+
+
+ ); +}; diff --git a/client/src/app/(main)/profile/ui/list.module.scss b/client/src/app/(main)/profile/ui/list.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/client/src/app/(main)/profile/ui/list.tsx b/client/src/app/(main)/profile/ui/list.tsx new file mode 100644 index 00000000..2a464df9 --- /dev/null +++ b/client/src/app/(main)/profile/ui/list.tsx @@ -0,0 +1,19 @@ +import { useGetMe } from '@/entities/session'; +import { Card } from './card'; +import { ChecksIcon, TrophyIcon, UserIcon } from '@/shared/assets'; +import { Row } from '@/app/(main)/profile/ui/row'; + +export const List = () => { + const { data: user } = useGetMe(); + + return ( + +
+ } text={user?.speciality ?? ''} /> + } text={user?.experience ?? ''} /> + } text={user?.country ?? ''} /> + } text={String(user?.dateOfBirth) ?? ''} /> +
+
+ ); +}; diff --git a/client/src/app/(main)/profile/ui/row.module.scss b/client/src/app/(main)/profile/ui/row.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/client/src/app/(main)/profile/ui/row.tsx b/client/src/app/(main)/profile/ui/row.tsx new file mode 100644 index 00000000..ae85d251 --- /dev/null +++ b/client/src/app/(main)/profile/ui/row.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; +import { Flex, Typography } from '@/shared/ui'; + +interface RowProps { + icon: ReactNode; + text: string; +} + +export const Row = ({ icon, text }: RowProps) => { + return ( + +
{icon}
+ {text} +
+ ); +}; From 903345ce869331813821b71dddd6069ab5365942 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 13:59:52 +0100 Subject: [PATCH 04/67] feat: add profile list icons --- client/src/app/(main)/profile/ui/list.tsx | 13 +++++++------ client/src/shared/assets/icons/cake.tsx | 15 +++++++++++++++ client/src/shared/assets/icons/index.ts | 3 +++ client/src/shared/assets/icons/map-pin.tsx | 13 +++++++++++++ client/src/shared/assets/icons/star.tsx | 12 ++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 client/src/shared/assets/icons/cake.tsx create mode 100644 client/src/shared/assets/icons/map-pin.tsx create mode 100644 client/src/shared/assets/icons/star.tsx diff --git a/client/src/app/(main)/profile/ui/list.tsx b/client/src/app/(main)/profile/ui/list.tsx index 2a464df9..c9e3d7e8 100644 --- a/client/src/app/(main)/profile/ui/list.tsx +++ b/client/src/app/(main)/profile/ui/list.tsx @@ -1,19 +1,20 @@ import { useGetMe } from '@/entities/session'; import { Card } from './card'; -import { ChecksIcon, TrophyIcon, UserIcon } from '@/shared/assets'; +import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; import { Row } from '@/app/(main)/profile/ui/row'; +import { Flex } from '@/shared/ui'; export const List = () => { const { data: user } = useGetMe(); return ( -
+ } text={user?.speciality ?? ''} /> - } text={user?.experience ?? ''} /> - } text={user?.country ?? ''} /> - } text={String(user?.dateOfBirth) ?? ''} /> -
+ } text={user?.experience ?? ''} /> + } text={user?.country ?? ''} /> + } text={String(user?.dateOfBirth) ?? ''} /> +
); }; diff --git a/client/src/shared/assets/icons/cake.tsx b/client/src/shared/assets/icons/cake.tsx new file mode 100644 index 00000000..964262d7 --- /dev/null +++ b/client/src/shared/assets/icons/cake.tsx @@ -0,0 +1,15 @@ + +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const Cake: FC = ({ size = '24', ...rest }) => { + return ( + + + + + + + + ); +}; diff --git a/client/src/shared/assets/icons/index.ts b/client/src/shared/assets/icons/index.ts index 1c7bd294..b1dea274 100644 --- a/client/src/shared/assets/icons/index.ts +++ b/client/src/shared/assets/icons/index.ts @@ -1,3 +1,6 @@ +export { Star } from './star'; +export { Cake } from './cake'; +export { MapPin } from './map-pin'; export { CheckIcon } from './check'; export { CrossIcon } from './cross'; export { EyeIcon } from './eye'; diff --git a/client/src/shared/assets/icons/map-pin.tsx b/client/src/shared/assets/icons/map-pin.tsx new file mode 100644 index 00000000..0e00b5cc --- /dev/null +++ b/client/src/shared/assets/icons/map-pin.tsx @@ -0,0 +1,13 @@ + +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const MapPin: FC = ({ size = '24', ...rest }) => { + return ( + + + + ); +}; + + diff --git a/client/src/shared/assets/icons/star.tsx b/client/src/shared/assets/icons/star.tsx new file mode 100644 index 00000000..32bb0b01 --- /dev/null +++ b/client/src/shared/assets/icons/star.tsx @@ -0,0 +1,12 @@ + +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const Star: FC = ({ size = '24', ...rest }) => { + return ( + + + + ); +}; + From 2eddf7113dc94bd25f7f64b97b1ad067298ff5f0 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 16:02:27 +0100 Subject: [PATCH 05/67] feat: add icons --- client/src/shared/assets/icons/behance.tsx | 25 ++++++++++ .../src/shared/assets/icons/github-icon.tsx | 23 +++++++++ client/src/shared/assets/icons/linkedin.tsx | 48 +++++++++++++++++++ client/src/shared/assets/icons/telegram.tsx | 39 +++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 client/src/shared/assets/icons/behance.tsx create mode 100644 client/src/shared/assets/icons/github-icon.tsx create mode 100644 client/src/shared/assets/icons/linkedin.tsx create mode 100644 client/src/shared/assets/icons/telegram.tsx diff --git a/client/src/shared/assets/icons/behance.tsx b/client/src/shared/assets/icons/behance.tsx new file mode 100644 index 00000000..c6a59d98 --- /dev/null +++ b/client/src/shared/assets/icons/behance.tsx @@ -0,0 +1,25 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const BehanceIcon: FC = ({ size = '28', ...rest }) => { + return ( + + + + ); +}; diff --git a/client/src/shared/assets/icons/github-icon.tsx b/client/src/shared/assets/icons/github-icon.tsx new file mode 100644 index 00000000..34b7d4ea --- /dev/null +++ b/client/src/shared/assets/icons/github-icon.tsx @@ -0,0 +1,23 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const GithubIcon: FC = ({ size = '28', ...rest }) => { + return ( + + + + ); +}; diff --git a/client/src/shared/assets/icons/linkedin.tsx b/client/src/shared/assets/icons/linkedin.tsx new file mode 100644 index 00000000..18c2aa52 --- /dev/null +++ b/client/src/shared/assets/icons/linkedin.tsx @@ -0,0 +1,48 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const LinkedinIcon: FC = ({ size = '28', ...rest }) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/client/src/shared/assets/icons/telegram.tsx b/client/src/shared/assets/icons/telegram.tsx new file mode 100644 index 00000000..06fa335e --- /dev/null +++ b/client/src/shared/assets/icons/telegram.tsx @@ -0,0 +1,39 @@ +import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; +import { FC } from 'react'; + +export const TelegramIcon: FC = ({ size = '28', ...rest }) => { + return ( + + + + ); +}; From cc5b0f21ba4c9ffe2fbcb937ff2e774e5bc3552e Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 16:03:19 +0100 Subject: [PATCH 06/67] feat: add icons --- client/src/shared/assets/icons/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/shared/assets/icons/index.ts b/client/src/shared/assets/icons/index.ts index b1dea274..371c0f8b 100644 --- a/client/src/shared/assets/icons/index.ts +++ b/client/src/shared/assets/icons/index.ts @@ -1,3 +1,7 @@ +import { Github } from './github-icon'; +import { BehanceIcon } from './behance'; +import { LinkedinIcon } from './linkedin'; +import { TelegramIcon } from './telegram'; export { Star } from './star'; export { Cake } from './cake'; export { MapPin } from './map-pin'; From 03ea13bb818c7c6dfb9110be73d773647f605733 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 16:04:21 +0100 Subject: [PATCH 07/67] feat: add social links --- client/src/app/(main)/profile/ui/about.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/src/app/(main)/profile/ui/about.tsx b/client/src/app/(main)/profile/ui/about.tsx index a4c67d54..b06bdce1 100644 --- a/client/src/app/(main)/profile/ui/about.tsx +++ b/client/src/app/(main)/profile/ui/about.tsx @@ -1,20 +1,28 @@ import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Flex, Typography } from '@/shared/ui'; -import { Github, Google } from '@/shared/assets'; +import { GithubIcon } from '@/shared/assets/icons/github-icon'; +import { BehanceIcon } from '@/shared/assets/icons/behance'; +import { TelegramIcon } from '@/shared/assets/icons/telegram'; +import { LinkedinIcon } from '@/shared/assets/icons/linkedin'; export const About = () => { const { data: user } = useGetMe(); + return ( - + About + {user?.description ?? 'Description'} + - - + {user?.links?.github && } + {user?.links?.behance && } + {user?.links?.telegram && } + {user?.links?.linkedIn && } From 84be41b14b7ca1f358ad200c74f595c7fdb3750a Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 16:05:01 +0100 Subject: [PATCH 08/67] feat: add logo --- client/src/app/(main)/profile/layout.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/app/(main)/profile/layout.tsx b/client/src/app/(main)/profile/layout.tsx index 9b949473..0a581ee2 100644 --- a/client/src/app/(main)/profile/layout.tsx +++ b/client/src/app/(main)/profile/layout.tsx @@ -6,6 +6,7 @@ import { Flex } from '@/shared/ui'; import { List } from './ui/list'; import { Skeleton } from '@/shared/ui/skeleton/skeleton'; import { About } from './ui/about'; +import { LogoBig } from '@/shared/assets'; export default function Layout() { const { data: user } = useGetMe(); @@ -21,6 +22,9 @@ export default function Layout() { return (
+ + +
{body} From 8000501da6b38cde65a014e709b9a80c6efc4e86 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:27:35 +0100 Subject: [PATCH 09/67] fix: layout behind aside menu --- client/src/app/(main)/layout.module.scss | 22 +++++++++++-------- client/src/app/(main)/layout.tsx | 1 + .../src/app/(main)/profile/layout.module.scss | 1 + 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/client/src/app/(main)/layout.module.scss b/client/src/app/(main)/layout.module.scss index e19c563e..810601ae 100644 --- a/client/src/app/(main)/layout.module.scss +++ b/client/src/app/(main)/layout.module.scss @@ -1,8 +1,17 @@ .container { height: 100dvh; width: 100%; - padding: 48px 55px; + + display: flex; +} + +.children { + width: 100%; + min-height: 100%; + display: flex; + flex-direction: column; + padding: 48px 55px; @media (width <= 1120px) { padding: 48px 24px; } @@ -12,11 +21,6 @@ } } -.children { - width: 100%; - min-height: 100%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} +.placeholder { + width: 93px; +} \ No newline at end of file diff --git a/client/src/app/(main)/layout.tsx b/client/src/app/(main)/layout.tsx index f487f025..d3594de7 100644 --- a/client/src/app/(main)/layout.tsx +++ b/client/src/app/(main)/layout.tsx @@ -16,6 +16,7 @@ export default function AuthLayout({ children }: { children: ReactNode }) { return (
+
{children}
); diff --git a/client/src/app/(main)/profile/layout.module.scss b/client/src/app/(main)/profile/layout.module.scss index 69e646a5..4d677e5d 100644 --- a/client/src/app/(main)/profile/layout.module.scss +++ b/client/src/app/(main)/profile/layout.module.scss @@ -2,6 +2,7 @@ .container { display: flex; width: 100%; + height: 100%; .body { width: 100%; From 67c0f50efa0fd24781ed23fa0edb18781e40acff Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:49:05 +0100 Subject: [PATCH 10/67] feat: add back button --- client/src/app/(main)/profile/layout.module.scss | 13 +++++++++++++ client/src/app/(main)/profile/layout.tsx | 10 +++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/client/src/app/(main)/profile/layout.module.scss b/client/src/app/(main)/profile/layout.module.scss index 4d677e5d..e54bbbf8 100644 --- a/client/src/app/(main)/profile/layout.module.scss +++ b/client/src/app/(main)/profile/layout.module.scss @@ -8,3 +8,16 @@ width: 100%; } } + +.back { + position: absolute; + left: -20px; + top: 0; + display: flex; + align-items: center; + gap: 6px; + &:hover { + opacity: .7; + transition: .3s; + } +} \ No newline at end of file diff --git a/client/src/app/(main)/profile/layout.tsx b/client/src/app/(main)/profile/layout.tsx index 0a581ee2..9fe1c1a3 100644 --- a/client/src/app/(main)/profile/layout.tsx +++ b/client/src/app/(main)/profile/layout.tsx @@ -2,13 +2,15 @@ import styles from './layout.module.scss'; import { useGetMe } from '@/entities/session'; import { Header } from './ui/header'; -import { Flex } from '@/shared/ui'; +import { Flex, Typography } from '@/shared/ui'; import { List } from './ui/list'; import { Skeleton } from '@/shared/ui/skeleton/skeleton'; import { About } from './ui/about'; -import { LogoBig } from '@/shared/assets'; +import { ArrowLeftIcon, LogoBig } from '@/shared/assets'; +import { useRouter } from 'next/navigation'; export default function Layout() { const { data: user } = useGetMe(); + const router = useRouter() let body = ; if (user) { @@ -19,9 +21,11 @@ export default function Layout() { ); } + return (
- + + From 1247de820a513ba54532363d65bf96bf56e58981 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 19:08:20 +0100 Subject: [PATCH 11/67] feat: add age --- client/src/app/(main)/profile/ui/list.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/app/(main)/profile/ui/list.tsx b/client/src/app/(main)/profile/ui/list.tsx index c9e3d7e8..52c37ad3 100644 --- a/client/src/app/(main)/profile/ui/list.tsx +++ b/client/src/app/(main)/profile/ui/list.tsx @@ -3,17 +3,23 @@ import { Card } from './card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; import { Row } from '@/app/(main)/profile/ui/row'; import { Flex } from '@/shared/ui'; +import { calculateAge } from '@/shared/lib'; export const List = () => { const { data: user } = useGetMe(); + let age = ""; + if (user?.dateOfBirth) { + age = (calculateAge(user.dateOfBirth)).toString(); + } + return ( } text={user?.speciality ?? ''} /> } text={user?.experience ?? ''} /> } text={user?.country ?? ''} /> - } text={String(user?.dateOfBirth) ?? ''} /> + {age && } text={age} /> } ); From 6ad790977d4f6e3e50b814a3b7f6c655b00f22ee Mon Sep 17 00:00:00 2001 From: Ivan Date: Sat, 3 Feb 2024 20:10:02 +0200 Subject: [PATCH 12/67] fix: add migration --- .../1706204678430-CreateFriendship.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 server/src/libs/database/migrations/1706204678430-CreateFriendship.ts diff --git a/server/src/libs/database/migrations/1706204678430-CreateFriendship.ts b/server/src/libs/database/migrations/1706204678430-CreateFriendship.ts new file mode 100644 index 00000000..428fb72c --- /dev/null +++ b/server/src/libs/database/migrations/1706204678430-CreateFriendship.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateFriendship1706204678430 implements MigrationInterface { + name = 'CreateFriendship1706204678430' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."friendship_status_enum" AS ENUM('accepted', 'pending', 'rejected')`); + await queryRunner.query(`CREATE TABLE "friendship" ("id" SERIAL NOT NULL, "status" "public"."friendship_status_enum" NOT NULL DEFAULT 'pending', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "creatorId" integer, "receiverId" integer, CONSTRAINT "PK_dbd6fb568cd912c5140307075cc" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('system', 'friend_request')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`); + await queryRunner.query(`ALTER TABLE "friendship" ADD CONSTRAINT "FK_9d7750b731557b149e689aeb63d" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "friendship" ADD CONSTRAINT "FK_1ce7870ad7e93284a3f186811f1" FOREIGN KEY ("receiverId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "friendship" DROP CONSTRAINT "FK_1ce7870ad7e93284a3f186811f1"`); + await queryRunner.query(`ALTER TABLE "friendship" DROP CONSTRAINT "FK_9d7750b731557b149e689aeb63d"`); + await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('system', 'team_invitation')`); + await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`); + await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`); + await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`); + await queryRunner.query(`DROP TABLE "friendship"`); + await queryRunner.query(`DROP TYPE "public"."friendship_status_enum"`); + } + +} From a69cc094a824f9a265d85822db61d253da05dc43 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 19:12:41 +0100 Subject: [PATCH 13/67] fix: remove profile id at the button link --- client/src/widgets/sidebar/config/getSidebarItems.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/widgets/sidebar/config/getSidebarItems.tsx b/client/src/widgets/sidebar/config/getSidebarItems.tsx index b7906966..9a62c156 100644 --- a/client/src/widgets/sidebar/config/getSidebarItems.tsx +++ b/client/src/widgets/sidebar/config/getSidebarItems.tsx @@ -20,7 +20,7 @@ export const getSidebarItems = (user?: IUserProtectedResponse) => { if (user) { data.push({ title: 'Profile', - path: `${PROFILE}/${user?.id}`, + path: `${PROFILE}`, icon: , }); } From 22ba88da7747019ba2b04ea06f80a051e2935b54 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 3 Feb 2024 19:18:40 +0100 Subject: [PATCH 14/67] fix: toggle leader icon based on fetch data --- client/src/app/(main)/profile/ui/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(main)/profile/ui/header.tsx b/client/src/app/(main)/profile/ui/header.tsx index e5e8a865..07a70c8d 100644 --- a/client/src/app/(main)/profile/ui/header.tsx +++ b/client/src/app/(main)/profile/ui/header.tsx @@ -19,7 +19,7 @@ export const Header = () => {
Date: Sun, 4 Feb 2024 19:49:54 +0100 Subject: [PATCH 15/67] feat: add empty placeholder for about section --- client/src/app/(main)/profile/ui/about.tsx | 49 +++++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/client/src/app/(main)/profile/ui/about.tsx b/client/src/app/(main)/profile/ui/about.tsx index b06bdce1..ad5f1d26 100644 --- a/client/src/app/(main)/profile/ui/about.tsx +++ b/client/src/app/(main)/profile/ui/about.tsx @@ -9,21 +9,48 @@ import { LinkedinIcon } from '@/shared/assets/icons/linkedin'; export const About = () => { const { data: user } = useGetMe(); + const linksPresent = Array.isArray(user?.links) && user.links.length > 0; + const descPresent = typeof user?.description === 'string'; return ( - - - About - + + {!descPresent && !linksPresent && ( + + No data{' '} + + )} + {descPresent && ( + + About + + )} - {user?.description ?? 'Description'} - - - {user?.links?.github && } - {user?.links?.behance && } - {user?.links?.telegram && } - {user?.links?.linkedIn && } + {descPresent && {user?.description}} + {linksPresent && ( + + {user?.links?.github && ( + + + + )} + {user?.links?.behance && ( + + + + )} + {user?.links?.telegram && ( + + + + )} + {user?.links?.linkedIn && ( + + + + )} + + )} ); From 4b78a22f6297c5fc2b45a97432fc5418c0a9ad24 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 6 Feb 2024 18:08:19 +0200 Subject: [PATCH 16/67] update: add check friendship status --- .../friendship/friendship.controller.ts | 8 ++++ .../modules/friendship/friendship.service.ts | 44 ++++++++++++++++++- .../friendship/types/friendship.types.ts | 6 +++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/server/src/modules/friendship/friendship.controller.ts b/server/src/modules/friendship/friendship.controller.ts index 21bf1b45..11963e82 100644 --- a/server/src/modules/friendship/friendship.controller.ts +++ b/server/src/modules/friendship/friendship.controller.ts @@ -48,6 +48,14 @@ export class FriendshipController { await this.friendshipService.updateStatus(creatorId, req.user.id, dto); } + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @HttpCode(HttpStatus.OK) + @Get('/status/:friendId') + async getStatus(@Param('friendId') userId: number, @Request() req) { + return await this.friendshipService.getStatus(req.user.id, userId); + } + @ApiBearerAuth() @UseGuards(AuthGuard('jwt')) @HttpCode(HttpStatus.NO_CONTENT) diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts index f88dc026..9d8399af 100644 --- a/server/src/modules/friendship/friendship.service.ts +++ b/server/src/modules/friendship/friendship.service.ts @@ -6,7 +6,7 @@ import { NotificationsService } from '../notifications/notifications.service'; import { UsersService } from '../users/users.service'; import { EntityCondition } from '../../utils/types/entity-condition.type'; import { NullableType } from '../../utils/types/nullable.type'; -import { FriendshipStatusTypes } from './types/friendship.types'; +import { FriendshipCheckStatusTypes, FriendshipStatusTypes } from './types/friendship.types'; import { UpdateStatusDto } from './dto/update-status.dto'; import { FriendRequestNotificationData, @@ -458,4 +458,46 @@ export class FriendshipService { ), }); } + + async getStatus(userId: number, friendId: number) { + const friendshipFrom = await this.findOne({ + creator: { id: userId }, + receiver: { id: friendId }, + }); + if (friendshipFrom && friendshipFrom.status === FriendshipStatusTypes.accepted) { + return { + status: FriendshipCheckStatusTypes.friends, + }; + } else if ( + friendshipFrom && + (friendshipFrom.status === FriendshipStatusTypes.pending || + friendshipFrom.status === FriendshipStatusTypes.rejected) + ) { + return { + status: FriendshipCheckStatusTypes.requested, + }; + } + const friendshipTo = await this.findOne({ + creator: { id: friendId }, + receiver: { id: userId }, + }); + if (friendshipTo && friendshipTo.status === FriendshipStatusTypes.accepted) { + return { + status: FriendshipCheckStatusTypes.friends, + }; + } else if (friendshipTo && friendshipTo.status === FriendshipStatusTypes.pending) { + return { + status: FriendshipCheckStatusTypes.toRespond, + }; + } + + if ( + (!friendshipTo && !friendshipFrom) || + (friendshipTo && friendshipTo.status === FriendshipStatusTypes.rejected) + ) { + return { + status: FriendshipCheckStatusTypes.none, + }; + } + } } diff --git a/server/src/modules/friendship/types/friendship.types.ts b/server/src/modules/friendship/types/friendship.types.ts index d89c9347..e7403bbe 100644 --- a/server/src/modules/friendship/types/friendship.types.ts +++ b/server/src/modules/friendship/types/friendship.types.ts @@ -3,3 +3,9 @@ export enum FriendshipStatusTypes { pending = 'pending', rejected = 'rejected', } +export enum FriendshipCheckStatusTypes { + none = 'none', + friends = 'friends', + requested = 'requested', + toRespond = 'toRespond', +} From 5027de5a1cccebf80a120fd54769c0725336dca1 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:59:37 +0100 Subject: [PATCH 17/67] feat: add [username] to the path --- .../app/(main)/{ => [username]}/profile/layout.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/layout.tsx | 0 client/src/app/(main)/{ => [username]}/profile/page.tsx | 0 .../(main)/{ => [username]}/profile/ui/about.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/about.tsx | 0 .../app/(main)/{ => [username]}/profile/ui/card.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/card.tsx | 0 .../(main)/{ => [username]}/profile/ui/header.module.scss | 0 .../src/app/(main)/{ => [username]}/profile/ui/header.tsx | 0 .../app/(main)/{ => [username]}/profile/ui/list.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/list.tsx | 2 +- .../app/(main)/{ => [username]}/profile/ui/row.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/row.tsx | 0 client/src/entities/session/api/useGithub.tsx | 2 +- client/src/entities/session/api/useGoogle.tsx | 2 +- client/src/entities/session/api/useLogin.tsx | 2 +- client/src/shared/ui/input/input/input.tsx | 2 +- client/src/widgets/modals/info-modal/team/phone/phone.tsx | 4 ++-- .../widgets/sidebar/ui/sidebar-profile/sidebar-profile.tsx | 6 +++--- server/test/user/users.e2e-spec.ts | 4 ++-- 20 files changed, 12 insertions(+), 12 deletions(-) rename client/src/app/(main)/{ => [username]}/profile/layout.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/layout.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/page.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/about.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/about.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/card.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/card.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/header.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/header.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/list.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/list.tsx (92%) rename client/src/app/(main)/{ => [username]}/profile/ui/row.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/row.tsx (100%) diff --git a/client/src/app/(main)/profile/layout.module.scss b/client/src/app/(main)/[username]/profile/layout.module.scss similarity index 100% rename from client/src/app/(main)/profile/layout.module.scss rename to client/src/app/(main)/[username]/profile/layout.module.scss diff --git a/client/src/app/(main)/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx similarity index 100% rename from client/src/app/(main)/profile/layout.tsx rename to client/src/app/(main)/[username]/profile/layout.tsx diff --git a/client/src/app/(main)/profile/page.tsx b/client/src/app/(main)/[username]/profile/page.tsx similarity index 100% rename from client/src/app/(main)/profile/page.tsx rename to client/src/app/(main)/[username]/profile/page.tsx diff --git a/client/src/app/(main)/profile/ui/about.module.scss b/client/src/app/(main)/[username]/profile/ui/about.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/about.module.scss rename to client/src/app/(main)/[username]/profile/ui/about.module.scss diff --git a/client/src/app/(main)/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/about.tsx rename to client/src/app/(main)/[username]/profile/ui/about.tsx diff --git a/client/src/app/(main)/profile/ui/card.module.scss b/client/src/app/(main)/[username]/profile/ui/card.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/card.module.scss rename to client/src/app/(main)/[username]/profile/ui/card.module.scss diff --git a/client/src/app/(main)/profile/ui/card.tsx b/client/src/app/(main)/[username]/profile/ui/card.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/card.tsx rename to client/src/app/(main)/[username]/profile/ui/card.tsx diff --git a/client/src/app/(main)/profile/ui/header.module.scss b/client/src/app/(main)/[username]/profile/ui/header.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/header.module.scss rename to client/src/app/(main)/[username]/profile/ui/header.module.scss diff --git a/client/src/app/(main)/profile/ui/header.tsx b/client/src/app/(main)/[username]/profile/ui/header.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/header.tsx rename to client/src/app/(main)/[username]/profile/ui/header.tsx diff --git a/client/src/app/(main)/profile/ui/list.module.scss b/client/src/app/(main)/[username]/profile/ui/list.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/list.module.scss rename to client/src/app/(main)/[username]/profile/ui/list.module.scss diff --git a/client/src/app/(main)/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx similarity index 92% rename from client/src/app/(main)/profile/ui/list.tsx rename to client/src/app/(main)/[username]/profile/ui/list.tsx index 52c37ad3..d679c789 100644 --- a/client/src/app/(main)/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -1,7 +1,7 @@ import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; -import { Row } from '@/app/(main)/profile/ui/row'; +import { Row } from '@/app/(main)/[username]/profile/ui/row'; import { Flex } from '@/shared/ui'; import { calculateAge } from '@/shared/lib'; diff --git a/client/src/app/(main)/profile/ui/row.module.scss b/client/src/app/(main)/[username]/profile/ui/row.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/row.module.scss rename to client/src/app/(main)/[username]/profile/ui/row.module.scss diff --git a/client/src/app/(main)/profile/ui/row.tsx b/client/src/app/(main)/[username]/profile/ui/row.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/row.tsx rename to client/src/app/(main)/[username]/profile/ui/row.tsx diff --git a/client/src/entities/session/api/useGithub.tsx b/client/src/entities/session/api/useGithub.tsx index b763695f..a437c978 100644 --- a/client/src/entities/session/api/useGithub.tsx +++ b/client/src/entities/session/api/useGithub.tsx @@ -17,7 +17,7 @@ export const useGithub = () => { localStorage.setItem('token', data.data.token); Cookies.set('refreshToken', data.data.refreshToken); /* - * If user has username it means he already signed up, so we don't need to + * If user has [username] it means he already signed up, so we don't need to * redirect him to onboarding, otherwise we should. * */ if (user.username) { diff --git a/client/src/entities/session/api/useGoogle.tsx b/client/src/entities/session/api/useGoogle.tsx index 200e19fe..56bf88e6 100644 --- a/client/src/entities/session/api/useGoogle.tsx +++ b/client/src/entities/session/api/useGoogle.tsx @@ -16,7 +16,7 @@ export const useGoogle = () => { localStorage.setItem('token', data.data.token); Cookies.set('refreshToken', data.data.refreshToken); /* - * If user has username it means he already signed up, so we don't need to + * If user has [username] it means he already signed up, so we don't need to * redirect him to onboarding, otherwise we should. * */ if (user.username) { diff --git a/client/src/entities/session/api/useLogin.tsx b/client/src/entities/session/api/useLogin.tsx index 4b129519..5cf643a4 100644 --- a/client/src/entities/session/api/useLogin.tsx +++ b/client/src/entities/session/api/useLogin.tsx @@ -17,7 +17,7 @@ export const useLogin = () => { localStorage.setItem('token', data.data.token); Cookies.set('refreshToken', data.data.refreshToken); /* - * If user has username it means he already signed up, so we don't need to + * If user has [username] it means he already signed up, so we don't need to * redirect him to onboarding, otherwise we should. * */ if (user.username) { diff --git a/client/src/shared/ui/input/input/input.tsx b/client/src/shared/ui/input/input/input.tsx index 30325096..c019065b 100644 --- a/client/src/shared/ui/input/input/input.tsx +++ b/client/src/shared/ui/input/input/input.tsx @@ -31,7 +31,7 @@ import styles from './input.module.scss'; * To access the underlying input element directly: * ```tsx * const inputRef = useRef(null); - * + * * ``` * */ diff --git a/client/src/widgets/modals/info-modal/team/phone/phone.tsx b/client/src/widgets/modals/info-modal/team/phone/phone.tsx index 87bfc5d3..f2951ab3 100644 --- a/client/src/widgets/modals/info-modal/team/phone/phone.tsx +++ b/client/src/widgets/modals/info-modal/team/phone/phone.tsx @@ -102,7 +102,7 @@ // // // -// {team?.leader?.username} +// {team?.leader?.[username]} // // // // -// {teammate?.username} +// {teammate?.[username]} // { }); }); - it('Register new user with username for tests: /api/v1/auth/email/register (POST)', async () => { + it('Register new user with [username] for tests: /api/v1/auth/email/register (POST)', async () => { const email = faker.internet.email(); await request(app) .post('/api/v1/auth/email/register') @@ -261,7 +261,7 @@ describe('Get users (e2e)', () => { }); }); - it('Get users with username filter: /api/v1/users?filters= (GET)', () => { + it('Get users with [username] filter: /api/v1/users?filters= (GET)', () => { return request(app) .get(`/api/v1/users?filters={"username": "${username}"}`) .expect(200) From cd96263eb4559be04b872bba7b76571cd552ee3f Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:10:44 +0100 Subject: [PATCH 18/67] Revert "feat: add [username] to the path" This reverts commit 5027de5a1cccebf80a120fd54769c0725336dca1. --- .../app/(main)/{[username] => }/profile/layout.module.scss | 0 client/src/app/(main)/{[username] => }/profile/layout.tsx | 0 client/src/app/(main)/{[username] => }/profile/page.tsx | 0 .../(main)/{[username] => }/profile/ui/about.module.scss | 0 client/src/app/(main)/{[username] => }/profile/ui/about.tsx | 0 .../app/(main)/{[username] => }/profile/ui/card.module.scss | 0 client/src/app/(main)/{[username] => }/profile/ui/card.tsx | 0 .../(main)/{[username] => }/profile/ui/header.module.scss | 0 .../src/app/(main)/{[username] => }/profile/ui/header.tsx | 0 .../app/(main)/{[username] => }/profile/ui/list.module.scss | 0 client/src/app/(main)/{[username] => }/profile/ui/list.tsx | 2 +- .../app/(main)/{[username] => }/profile/ui/row.module.scss | 0 client/src/app/(main)/{[username] => }/profile/ui/row.tsx | 0 client/src/entities/session/api/useGithub.tsx | 2 +- client/src/entities/session/api/useGoogle.tsx | 2 +- client/src/entities/session/api/useLogin.tsx | 2 +- client/src/shared/ui/input/input/input.tsx | 2 +- client/src/widgets/modals/info-modal/team/phone/phone.tsx | 4 ++-- .../widgets/sidebar/ui/sidebar-profile/sidebar-profile.tsx | 6 +++--- server/test/user/users.e2e-spec.ts | 4 ++-- 20 files changed, 12 insertions(+), 12 deletions(-) rename client/src/app/(main)/{[username] => }/profile/layout.module.scss (100%) rename client/src/app/(main)/{[username] => }/profile/layout.tsx (100%) rename client/src/app/(main)/{[username] => }/profile/page.tsx (100%) rename client/src/app/(main)/{[username] => }/profile/ui/about.module.scss (100%) rename client/src/app/(main)/{[username] => }/profile/ui/about.tsx (100%) rename client/src/app/(main)/{[username] => }/profile/ui/card.module.scss (100%) rename client/src/app/(main)/{[username] => }/profile/ui/card.tsx (100%) rename client/src/app/(main)/{[username] => }/profile/ui/header.module.scss (100%) rename client/src/app/(main)/{[username] => }/profile/ui/header.tsx (100%) rename client/src/app/(main)/{[username] => }/profile/ui/list.module.scss (100%) rename client/src/app/(main)/{[username] => }/profile/ui/list.tsx (92%) rename client/src/app/(main)/{[username] => }/profile/ui/row.module.scss (100%) rename client/src/app/(main)/{[username] => }/profile/ui/row.tsx (100%) diff --git a/client/src/app/(main)/[username]/profile/layout.module.scss b/client/src/app/(main)/profile/layout.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/layout.module.scss rename to client/src/app/(main)/profile/layout.module.scss diff --git a/client/src/app/(main)/[username]/profile/layout.tsx b/client/src/app/(main)/profile/layout.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/layout.tsx rename to client/src/app/(main)/profile/layout.tsx diff --git a/client/src/app/(main)/[username]/profile/page.tsx b/client/src/app/(main)/profile/page.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/page.tsx rename to client/src/app/(main)/profile/page.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/about.module.scss b/client/src/app/(main)/profile/ui/about.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/about.module.scss rename to client/src/app/(main)/profile/ui/about.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/about.tsx b/client/src/app/(main)/profile/ui/about.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/about.tsx rename to client/src/app/(main)/profile/ui/about.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/card.module.scss b/client/src/app/(main)/profile/ui/card.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/card.module.scss rename to client/src/app/(main)/profile/ui/card.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/card.tsx b/client/src/app/(main)/profile/ui/card.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/card.tsx rename to client/src/app/(main)/profile/ui/card.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/header.module.scss b/client/src/app/(main)/profile/ui/header.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/header.module.scss rename to client/src/app/(main)/profile/ui/header.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/header.tsx b/client/src/app/(main)/profile/ui/header.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/header.tsx rename to client/src/app/(main)/profile/ui/header.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/list.module.scss b/client/src/app/(main)/profile/ui/list.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/list.module.scss rename to client/src/app/(main)/profile/ui/list.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/profile/ui/list.tsx similarity index 92% rename from client/src/app/(main)/[username]/profile/ui/list.tsx rename to client/src/app/(main)/profile/ui/list.tsx index d679c789..52c37ad3 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/profile/ui/list.tsx @@ -1,7 +1,7 @@ import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; -import { Row } from '@/app/(main)/[username]/profile/ui/row'; +import { Row } from '@/app/(main)/profile/ui/row'; import { Flex } from '@/shared/ui'; import { calculateAge } from '@/shared/lib'; diff --git a/client/src/app/(main)/[username]/profile/ui/row.module.scss b/client/src/app/(main)/profile/ui/row.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/row.module.scss rename to client/src/app/(main)/profile/ui/row.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/row.tsx b/client/src/app/(main)/profile/ui/row.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/row.tsx rename to client/src/app/(main)/profile/ui/row.tsx diff --git a/client/src/entities/session/api/useGithub.tsx b/client/src/entities/session/api/useGithub.tsx index a437c978..b763695f 100644 --- a/client/src/entities/session/api/useGithub.tsx +++ b/client/src/entities/session/api/useGithub.tsx @@ -17,7 +17,7 @@ export const useGithub = () => { localStorage.setItem('token', data.data.token); Cookies.set('refreshToken', data.data.refreshToken); /* - * If user has [username] it means he already signed up, so we don't need to + * If user has username it means he already signed up, so we don't need to * redirect him to onboarding, otherwise we should. * */ if (user.username) { diff --git a/client/src/entities/session/api/useGoogle.tsx b/client/src/entities/session/api/useGoogle.tsx index 56bf88e6..200e19fe 100644 --- a/client/src/entities/session/api/useGoogle.tsx +++ b/client/src/entities/session/api/useGoogle.tsx @@ -16,7 +16,7 @@ export const useGoogle = () => { localStorage.setItem('token', data.data.token); Cookies.set('refreshToken', data.data.refreshToken); /* - * If user has [username] it means he already signed up, so we don't need to + * If user has username it means he already signed up, so we don't need to * redirect him to onboarding, otherwise we should. * */ if (user.username) { diff --git a/client/src/entities/session/api/useLogin.tsx b/client/src/entities/session/api/useLogin.tsx index 5cf643a4..4b129519 100644 --- a/client/src/entities/session/api/useLogin.tsx +++ b/client/src/entities/session/api/useLogin.tsx @@ -17,7 +17,7 @@ export const useLogin = () => { localStorage.setItem('token', data.data.token); Cookies.set('refreshToken', data.data.refreshToken); /* - * If user has [username] it means he already signed up, so we don't need to + * If user has username it means he already signed up, so we don't need to * redirect him to onboarding, otherwise we should. * */ if (user.username) { diff --git a/client/src/shared/ui/input/input/input.tsx b/client/src/shared/ui/input/input/input.tsx index c019065b..30325096 100644 --- a/client/src/shared/ui/input/input/input.tsx +++ b/client/src/shared/ui/input/input/input.tsx @@ -31,7 +31,7 @@ import styles from './input.module.scss'; * To access the underlying input element directly: * ```tsx * const inputRef = useRef(null); - * + * * ``` * */ diff --git a/client/src/widgets/modals/info-modal/team/phone/phone.tsx b/client/src/widgets/modals/info-modal/team/phone/phone.tsx index f2951ab3..87bfc5d3 100644 --- a/client/src/widgets/modals/info-modal/team/phone/phone.tsx +++ b/client/src/widgets/modals/info-modal/team/phone/phone.tsx @@ -102,7 +102,7 @@ // // // -// {team?.leader?.[username]} +// {team?.leader?.username} // // // // -// {teammate?.[username]} +// {teammate?.username} // { }); }); - it('Register new user with [username] for tests: /api/v1/auth/email/register (POST)', async () => { + it('Register new user with username for tests: /api/v1/auth/email/register (POST)', async () => { const email = faker.internet.email(); await request(app) .post('/api/v1/auth/email/register') @@ -261,7 +261,7 @@ describe('Get users (e2e)', () => { }); }); - it('Get users with [username] filter: /api/v1/users?filters= (GET)', () => { + it('Get users with username filter: /api/v1/users?filters= (GET)', () => { return request(app) .get(`/api/v1/users?filters={"username": "${username}"}`) .expect(200) From a4bddc00b39154aed1c90f87f4d290dad959cef0 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:10:24 +0100 Subject: [PATCH 19/67] fix: change route to the profile page --- .../src/app/(main)/{ => [username]}/profile/layout.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/layout.tsx | 0 client/src/app/(main)/{ => [username]}/profile/page.tsx | 0 .../app/(main)/{ => [username]}/profile/ui/about.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/about.tsx | 0 .../app/(main)/{ => [username]}/profile/ui/card.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/card.tsx | 0 .../app/(main)/{ => [username]}/profile/ui/header.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/header.tsx | 0 .../app/(main)/{ => [username]}/profile/ui/list.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/list.tsx | 3 ++- .../src/app/(main)/{ => [username]}/profile/ui/row.module.scss | 0 client/src/app/(main)/{ => [username]}/profile/ui/row.tsx | 0 client/src/widgets/sidebar/config/getSidebarItems.tsx | 2 +- 14 files changed, 3 insertions(+), 2 deletions(-) rename client/src/app/(main)/{ => [username]}/profile/layout.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/layout.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/page.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/about.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/about.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/card.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/card.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/header.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/header.tsx (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/list.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/list.tsx (88%) rename client/src/app/(main)/{ => [username]}/profile/ui/row.module.scss (100%) rename client/src/app/(main)/{ => [username]}/profile/ui/row.tsx (100%) diff --git a/client/src/app/(main)/profile/layout.module.scss b/client/src/app/(main)/[username]/profile/layout.module.scss similarity index 100% rename from client/src/app/(main)/profile/layout.module.scss rename to client/src/app/(main)/[username]/profile/layout.module.scss diff --git a/client/src/app/(main)/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx similarity index 100% rename from client/src/app/(main)/profile/layout.tsx rename to client/src/app/(main)/[username]/profile/layout.tsx diff --git a/client/src/app/(main)/profile/page.tsx b/client/src/app/(main)/[username]/profile/page.tsx similarity index 100% rename from client/src/app/(main)/profile/page.tsx rename to client/src/app/(main)/[username]/profile/page.tsx diff --git a/client/src/app/(main)/profile/ui/about.module.scss b/client/src/app/(main)/[username]/profile/ui/about.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/about.module.scss rename to client/src/app/(main)/[username]/profile/ui/about.module.scss diff --git a/client/src/app/(main)/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/about.tsx rename to client/src/app/(main)/[username]/profile/ui/about.tsx diff --git a/client/src/app/(main)/profile/ui/card.module.scss b/client/src/app/(main)/[username]/profile/ui/card.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/card.module.scss rename to client/src/app/(main)/[username]/profile/ui/card.module.scss diff --git a/client/src/app/(main)/profile/ui/card.tsx b/client/src/app/(main)/[username]/profile/ui/card.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/card.tsx rename to client/src/app/(main)/[username]/profile/ui/card.tsx diff --git a/client/src/app/(main)/profile/ui/header.module.scss b/client/src/app/(main)/[username]/profile/ui/header.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/header.module.scss rename to client/src/app/(main)/[username]/profile/ui/header.module.scss diff --git a/client/src/app/(main)/profile/ui/header.tsx b/client/src/app/(main)/[username]/profile/ui/header.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/header.tsx rename to client/src/app/(main)/[username]/profile/ui/header.tsx diff --git a/client/src/app/(main)/profile/ui/list.module.scss b/client/src/app/(main)/[username]/profile/ui/list.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/list.module.scss rename to client/src/app/(main)/[username]/profile/ui/list.module.scss diff --git a/client/src/app/(main)/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx similarity index 88% rename from client/src/app/(main)/profile/ui/list.tsx rename to client/src/app/(main)/[username]/profile/ui/list.tsx index 52c37ad3..2aad34d6 100644 --- a/client/src/app/(main)/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -1,7 +1,8 @@ import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; -import { Row } from '@/app/(main)/profile/ui/row'; +// import { Row } from '@/app/(main)/user/[username]/profile/ui/row'; +import { Row } from "./row" import { Flex } from '@/shared/ui'; import { calculateAge } from '@/shared/lib'; diff --git a/client/src/app/(main)/profile/ui/row.module.scss b/client/src/app/(main)/[username]/profile/ui/row.module.scss similarity index 100% rename from client/src/app/(main)/profile/ui/row.module.scss rename to client/src/app/(main)/[username]/profile/ui/row.module.scss diff --git a/client/src/app/(main)/profile/ui/row.tsx b/client/src/app/(main)/[username]/profile/ui/row.tsx similarity index 100% rename from client/src/app/(main)/profile/ui/row.tsx rename to client/src/app/(main)/[username]/profile/ui/row.tsx diff --git a/client/src/widgets/sidebar/config/getSidebarItems.tsx b/client/src/widgets/sidebar/config/getSidebarItems.tsx index 9a62c156..eb517110 100644 --- a/client/src/widgets/sidebar/config/getSidebarItems.tsx +++ b/client/src/widgets/sidebar/config/getSidebarItems.tsx @@ -20,7 +20,7 @@ export const getSidebarItems = (user?: IUserProtectedResponse) => { if (user) { data.push({ title: 'Profile', - path: `${PROFILE}`, + path: `${user?.username}/${PROFILE}`, icon: , }); } From 91136efa5116feb165b754a02fece5eb1e9e4a88 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:26:21 +0100 Subject: [PATCH 20/67] feat: add empty state --- .../src/app/(main)/[username]/profile/ui/about.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about.tsx index ad5f1d26..49b96f97 100644 --- a/client/src/app/(main)/[username]/profile/ui/about.tsx +++ b/client/src/app/(main)/[username]/profile/ui/about.tsx @@ -14,16 +14,14 @@ export const About = () => { return ( - {!descPresent && !linksPresent && ( - - No data{' '} - - )} - {descPresent && ( + About + {descPresent ? ( About - )} + ) : + No description added. + } {descPresent && {user?.description}} From a32e3bbe51620ab6d9e5d1a80187244fd0dfcae0 Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 9 Feb 2024 20:54:40 +0200 Subject: [PATCH 21/67] update: reworked notification update --- .../modules/friendship/friendship.service.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts index 9d8399af..827c4902 100644 --- a/server/src/modules/friendship/friendship.service.ts +++ b/server/src/modules/friendship/friendship.service.ts @@ -263,24 +263,24 @@ export class FriendshipService { HttpStatus.NOT_FOUND ); } - const notification = await this.notificationsService.findOne({ - receiver: { id: receiverId }, - data: { status: NotificationStatusEnum.pending, creator: creator.toJSON() }, - }); - if (!notification) { - throw new HttpException( - { - status: HttpStatus.NOT_FOUND, - errors: { - notification: `notification not found`, - }, - }, - HttpStatus.NOT_FOUND - ); - } - (notification.data as FriendRequestNotificationData).status = - NotificationStatusEnum[dto.status]; - await this.notificationsService.save(notification); + // const notification = await this.notificationsService.findOne({ + // receiver: { id: receiverId }, + // data: { status: NotificationStatusEnum.pending, creator: creator.toJSON() }, + // }); + // if (!notification) { + // throw new HttpException( + // { + // status: HttpStatus.NOT_FOUND, + // errors: { + // notification: `notification not found`, + // }, + // }, + // HttpStatus.NOT_FOUND + // ); + // } + // (notification.data as FriendRequestNotificationData).status = + // NotificationStatusEnum[dto.status]; + // await this.notificationsService.save(notification); friendship.status = FriendshipStatusTypes[dto.status]; await this.friendshipRepository.save(friendship); From 93a278ac343c67edbc65471556802a8d15db930f Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 9 Feb 2024 21:14:36 +0200 Subject: [PATCH 22/67] update: updated friendship delete --- .../friendship/friendship.controller.ts | 12 +++++- .../modules/friendship/friendship.service.ts | 42 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/server/src/modules/friendship/friendship.controller.ts b/server/src/modules/friendship/friendship.controller.ts index 11963e82..46f4a844 100644 --- a/server/src/modules/friendship/friendship.controller.ts +++ b/server/src/modules/friendship/friendship.controller.ts @@ -60,8 +60,16 @@ export class FriendshipController { @UseGuards(AuthGuard('jwt')) @HttpCode(HttpStatus.NO_CONTENT) @Delete('/:friendId') - async deleteRequestOrFriendship(@Param('friendId') friendId: number, @Request() req) { - await this.friendshipService.deleteRequestOrFriendship(friendId, req.user.id); + async deleteFriendship(@Param('friendId') friendId: number, @Request() req) { + await this.friendshipService.deleteFriendship(friendId, req.user.id); + } + + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @HttpCode(HttpStatus.OK) + @Patch('request/:friendId') + async rejectRequest(@Param('friendId') friendId: number, @Request() req) { + await this.friendshipService.rejectRequest(friendId, req.user.id); } @HttpCode(HttpStatus.OK) diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts index 827c4902..b0ee8fc7 100644 --- a/server/src/modules/friendship/friendship.service.ts +++ b/server/src/modules/friendship/friendship.service.ts @@ -298,7 +298,7 @@ export class FriendshipService { } } - async deleteRequestOrFriendship(friendId: number, userId: number) { + async deleteFriendship(friendId: number, userId: number) { if (userId === friendId) { throw new HttpException( { @@ -373,7 +373,47 @@ export class FriendshipService { await this.friendshipRepository.remove(friendshipsToDelete); return; } + } + + async rejectRequest(friendId: number, userId: number) { + if (userId === friendId) { + throw new HttpException( + { + status: HttpStatus.BAD_REQUEST, + errors: { + message: 'You can`t do this operation with yourself', + }, + }, + HttpStatus.BAD_REQUEST + ); + } + const user = await this.usersService.findOne({ id: userId }); + if (!user) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${user} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } + + const friend = await this.usersService.findOne({ id: friendId }); + + if (!friend) { + throw new HttpException( + { + status: HttpStatus.NOT_FOUND, + errors: { + user: `user with id: ${friendId} was not found`, + }, + }, + HttpStatus.NOT_FOUND + ); + } if ( await this.friendshipRepository.exist({ where: { From 71ddf1ab00aea9e10ba23d29e0f274740d7ff1e4 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:42:32 +0100 Subject: [PATCH 23/67] fix: incorrect image reference --- client/src/app/(main)/[username]/profile/ui/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(main)/[username]/profile/ui/header.tsx b/client/src/app/(main)/[username]/profile/ui/header.tsx index 07a70c8d..b51fe4d5 100644 --- a/client/src/app/(main)/[username]/profile/ui/header.tsx +++ b/client/src/app/(main)/[username]/profile/ui/header.tsx @@ -24,7 +24,7 @@ export const Header = () => { height={100} className={styles.image} borderRadius={'50%'} - src={String(user.photo ?? '/images/placeholder.png')} + src={String(user.photo?.path ?? '/images/placeholder.png')} alt={user.username ?? 'Profile picture'} />
From 793d96ffccb0e61f6bc2652cc2c8a5f2d2493886 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:43:23 +0100 Subject: [PATCH 24/67] fix: case when links are missing --- client/src/app/(main)/[username]/profile/ui/about.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about.tsx index 49b96f97..e75c7b26 100644 --- a/client/src/app/(main)/[username]/profile/ui/about.tsx +++ b/client/src/app/(main)/[username]/profile/ui/about.tsx @@ -8,18 +8,15 @@ import { LinkedinIcon } from '@/shared/assets/icons/linkedin'; export const About = () => { const { data: user } = useGetMe(); + console.log(user?.links?.github); - const linksPresent = Array.isArray(user?.links) && user.links.length > 0; + const linksPresent = user?.links && Object.keys(user.links).length; const descPresent = typeof user?.description === 'string'; return ( About - {descPresent ? ( - - About - - ) : + {!descPresent && No description added. } From c059777f22a595e66e1b105860a7d52927cd3d1a Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Fri, 9 Feb 2024 23:43:07 +0100 Subject: [PATCH 25/67] fix: minor ui changes --- client/src/app/(main)/[username]/profile/ui/header.module.scss | 1 + client/src/app/(main)/[username]/profile/ui/list.tsx | 2 +- client/src/shared/assets/icons/index.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/ui/header.module.scss b/client/src/app/(main)/[username]/profile/ui/header.module.scss index 19cf62e6..efe2dde7 100644 --- a/client/src/app/(main)/[username]/profile/ui/header.module.scss +++ b/client/src/app/(main)/[username]/profile/ui/header.module.scss @@ -3,6 +3,7 @@ overflow: hidden; border-radius: 15px; background: rgba(67, 71, 82); + flex-shrink: 0; } .background { diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx index 2aad34d6..953a1e9f 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -20,7 +20,7 @@ export const List = () => { } text={user?.speciality ?? ''} /> } text={user?.experience ?? ''} /> } text={user?.country ?? ''} /> - {age && } text={age} /> } + {age && } text={`${age} years old`} /> } ); diff --git a/client/src/shared/assets/icons/index.ts b/client/src/shared/assets/icons/index.ts index 371c0f8b..eddd1f8e 100644 --- a/client/src/shared/assets/icons/index.ts +++ b/client/src/shared/assets/icons/index.ts @@ -1,4 +1,4 @@ -import { Github } from './github-icon'; +import { GithubIcon } from './github-icon'; import { BehanceIcon } from './behance'; import { LinkedinIcon } from './linkedin'; import { TelegramIcon } from './telegram'; From 890255459fe36c964694f936ebe404eb276de121 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:30:18 +0100 Subject: [PATCH 26/67] refactor: run prettier --- .../(main)/[username]/profile/ui/about.tsx | 12 +++-- .../app/(main)/[username]/profile/ui/list.tsx | 10 ++-- client/src/shared/assets/icons/cake.tsx | 50 ++++++++++++++++--- client/src/shared/assets/icons/map-pin.tsx | 17 +++++-- client/src/shared/assets/icons/star.tsx | 16 ++++-- 5 files changed, 80 insertions(+), 25 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about.tsx index e75c7b26..d8e602d0 100644 --- a/client/src/app/(main)/[username]/profile/ui/about.tsx +++ b/client/src/app/(main)/[username]/profile/ui/about.tsx @@ -15,10 +15,14 @@ export const About = () => { return ( - About - {!descPresent && - No description added. - } + + About + + {!descPresent && ( + + No description added. + + )} {descPresent && {user?.description}} diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx index 953a1e9f..9d55ebb8 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -2,25 +2,25 @@ import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; // import { Row } from '@/app/(main)/user/[username]/profile/ui/row'; -import { Row } from "./row" +import { Row } from './row'; import { Flex } from '@/shared/ui'; import { calculateAge } from '@/shared/lib'; export const List = () => { const { data: user } = useGetMe(); - let age = ""; + let age = ''; if (user?.dateOfBirth) { - age = (calculateAge(user.dateOfBirth)).toString(); + age = calculateAge(user.dateOfBirth).toString(); } return ( - + } text={user?.speciality ?? ''} /> } text={user?.experience ?? ''} /> } text={user?.country ?? ''} /> - {age && } text={`${age} years old`} /> } + {age && } text={`${age} years old`} />} ); diff --git a/client/src/shared/assets/icons/cake.tsx b/client/src/shared/assets/icons/cake.tsx index 964262d7..eb9bd506 100644 --- a/client/src/shared/assets/icons/cake.tsx +++ b/client/src/shared/assets/icons/cake.tsx @@ -1,15 +1,51 @@ - import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; import { FC } from 'react'; export const Cake: FC = ({ size = '24', ...rest }) => { return ( - - - - - - + + + + + + ); }; diff --git a/client/src/shared/assets/icons/map-pin.tsx b/client/src/shared/assets/icons/map-pin.tsx index 0e00b5cc..cc98d227 100644 --- a/client/src/shared/assets/icons/map-pin.tsx +++ b/client/src/shared/assets/icons/map-pin.tsx @@ -1,13 +1,20 @@ - import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; import { FC } from 'react'; export const MapPin: FC = ({ size = '24', ...rest }) => { return ( - - + + ); }; - - diff --git a/client/src/shared/assets/icons/star.tsx b/client/src/shared/assets/icons/star.tsx index 32bb0b01..ab3877b7 100644 --- a/client/src/shared/assets/icons/star.tsx +++ b/client/src/shared/assets/icons/star.tsx @@ -1,12 +1,20 @@ - import { SVGPropsWithSize } from '@/shared/types/svg-props-with-size'; import { FC } from 'react'; export const Star: FC = ({ size = '24', ...rest }) => { return ( - - + + ); }; - From 2dd25d1fe9efb59d6c12a2ab77d8e67cf40f13a8 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:34:24 +0100 Subject: [PATCH 27/67] feat: add skills (except projects & tournaments) --- .../app/(main)/[username]/profile/layout.tsx | 26 +++++++-- .../(main)/[username]/profile/ui/Friends.tsx | 8 +++ .../[username]/profile/ui/fields.module.scss | 12 ++++ .../(main)/[username]/profile/ui/fields.tsx | 45 ++++++++++++++ .../profile/ui/fields/education.tsx | 35 +++++++++++ .../[username]/profile/ui/fields/skills.tsx | 58 +++++++++++++++++++ .../profile/ui/fields/work-experience.tsx | 30 ++++++++++ 7 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 client/src/app/(main)/[username]/profile/ui/Friends.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields.module.scss create mode 100644 client/src/app/(main)/[username]/profile/ui/fields.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/education.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/skills.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx diff --git a/client/src/app/(main)/[username]/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx index 9fe1c1a3..e07b0c22 100644 --- a/client/src/app/(main)/[username]/profile/layout.tsx +++ b/client/src/app/(main)/[username]/profile/layout.tsx @@ -7,17 +7,28 @@ import { List } from './ui/list'; import { Skeleton } from '@/shared/ui/skeleton/skeleton'; import { About } from './ui/about'; import { ArrowLeftIcon, LogoBig } from '@/shared/assets'; -import { useRouter } from 'next/navigation'; +import { useRouter, useParams } from 'next/navigation'; +import { Friends } from './ui/Friends'; +import { Fields } from './ui/fields'; export default function Layout() { const { data: user } = useGetMe(); - const router = useRouter() + const router = useRouter(); + const { username } = useParams(); + + const isMyProfile = user?.username === username; let body = ; if (user) { body = ( - - - + + + + + + + + + ); } @@ -25,7 +36,10 @@ export default function Layout() { return (
- + diff --git a/client/src/app/(main)/[username]/profile/ui/Friends.tsx b/client/src/app/(main)/[username]/profile/ui/Friends.tsx new file mode 100644 index 00000000..d293850a --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/Friends.tsx @@ -0,0 +1,8 @@ +import { useGetMe } from '@/entities/session'; +import { Card } from './card'; + +export const Friends = () => { + const { data: user } = useGetMe(); + + return Team here; +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields.module.scss b/client/src/app/(main)/[username]/profile/ui/fields.module.scss new file mode 100644 index 00000000..ba6a2a3d --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields.module.scss @@ -0,0 +1,12 @@ +.selected { + border-bottom: 1px solid #5bd424; +} + +.field_text { + transition: 0.3s; + transition-property: color; +} + +.fields_container { + min-height: 150px; +} \ No newline at end of file diff --git a/client/src/app/(main)/[username]/profile/ui/fields.tsx b/client/src/app/(main)/[username]/profile/ui/fields.tsx new file mode 100644 index 00000000..2106a25e --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields.tsx @@ -0,0 +1,45 @@ +import { useGetMe } from '@/entities/session'; +import { Card } from './card'; +import { Flex, Typography } from '@/shared/ui'; +import { useState } from 'react'; +import styles from './fields.module.scss'; +import { Skills } from './fields/skills'; +import { WorkExperience } from './fields/work-experience'; +import { Education } from './fields/education'; + +export const Fields = () => { + const { data: user } = useGetMe(); + + const [field, setField] = useState('Skills'); + + const fields = { + Skills: , + Projects: null, + 'Work experience': , + Education: , + Tournaments: null, + }; + + return ( + + + + {Object.keys(fields).map(key => { + const classProps = field === key ? { className: styles.selected } : {}; + return ( + + ); + })} + + {fields[field]} + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/education.tsx b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx new file mode 100644 index 00000000..7b3d9e53 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx @@ -0,0 +1,35 @@ +import { useGetMe } from '@/entities/session'; +import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; +import { useState } from 'react'; +import styles from './fields.module.scss'; + +export const Education = () => { + const { data: user } = useGetMe(); + const universities = user?.universities; + if (!universities) return No information; + return ( + + {universities.map((education, i) => { + const start = new Date(education.admissionDate).getFullYear(); + const end = education.graduationDate + ? new Date(education.graduationDate).getFullYear() + : 'Present'; + return ( + + + + {education.name ?? (education as unknown as { university: string }).university} + + + {education.degree} in {education.major} + + + + {start} - {end} + + + ); + })} + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx new file mode 100644 index 00000000..53632a1c --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -0,0 +1,58 @@ +import { useGetMe } from '@/entities/session'; +import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; +import { useState } from 'react'; +import styles from './fields.module.scss'; + +export const Skills = () => { + const { data: user } = useGetMe(); + + const skills = { + programmingLanguages: { + Badge: BadgeIcon, + title: 'Programming Languages', + }, + frameworks: { + Badge: BadgeText, + title: 'Frameworks', + }, + fields: { + Badge: BadgeText, + title: 'Fields', + }, + designerTools: { + Badge: BadgeIcon, + title: 'Fields', + }, + projectManagerTools: { + Badge: BadgeIcon, + title: 'Tools', + }, + methodologies: { + Badge: BadgeText, + title: 'Methodologies', + }, + }; + + return ( + + {user?.skills && + Object.entries(skills).map(skill => { + const skillName = skill[0] as keyof typeof skills; + if (!user.skills[skillName]) return null; + const Badge = skills[skillName].Badge; + return ( + + {skills[skillName].title} + {user?.skills && ( + + {user.skills[skillName].map((lang: string) => ( + + ))} + + )} + + ); + })} + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx b/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx new file mode 100644 index 00000000..65cce1bc --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx @@ -0,0 +1,30 @@ +import { useGetMe } from '@/entities/session'; +import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; +import { useState } from 'react'; +import styles from './fields.module.scss'; + +export const WorkExperience = () => { + const { data: user } = useGetMe(); + const jobs = user?.jobs; + return ( + + {jobs.map((job, i) => { + const start = new Date(job.startDate).getFullYear(); + const end = job.endDate ? new Date(job.endDate).getFullYear() : 'Present'; + return ( + + + {job.company} + + {job.title} + + + + {start} - {end} + + + ); + })} + + ); +}; From 79b553542aa155228f6fe00394cf6f765a097e5e Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 10 Feb 2024 22:23:06 +0100 Subject: [PATCH 28/67] feat: add friends button logics --- .../app/(main)/[username]/profile/layout.tsx | 62 +++++++++++-------- .../[username]/profile/lib/profile-context.ts | 3 + .../profile/lib/useGetUserByName.ts | 5 ++ .../(main)/[username]/profile/ui/Friends.tsx | 8 --- .../(main)/[username]/profile/ui/about.tsx | 7 ++- .../(main)/[username]/profile/ui/fields.tsx | 4 -- .../profile/ui/fields/education.tsx | 3 +- .../[username]/profile/ui/fields/skills.tsx | 13 ++-- .../profile/ui/fields/work-experience.tsx | 11 ++-- .../(main)/[username]/profile/ui/friends.tsx | 10 +++ .../(main)/[username]/profile/ui/header.tsx | 46 +++++++++++--- .../app/(main)/[username]/profile/ui/list.tsx | 5 +- client/src/entities/session/api/index.ts | 1 + .../src/entities/session/api/useAddFriend.tsx | 17 +++++ .../entities/session/api/useGetFriends.tsx | 19 ++++++ .../src/entities/session/api/useGetUsers.tsx | 6 +- client/src/shared/constant/server-routes.ts | 1 + 17 files changed, 151 insertions(+), 70 deletions(-) create mode 100644 client/src/app/(main)/[username]/profile/lib/profile-context.ts create mode 100644 client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts delete mode 100644 client/src/app/(main)/[username]/profile/ui/Friends.tsx create mode 100644 client/src/app/(main)/[username]/profile/ui/friends.tsx create mode 100644 client/src/entities/session/api/useAddFriend.tsx create mode 100644 client/src/entities/session/api/useGetFriends.tsx diff --git a/client/src/app/(main)/[username]/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx index e07b0c22..2d13a4d9 100644 --- a/client/src/app/(main)/[username]/profile/layout.tsx +++ b/client/src/app/(main)/[username]/profile/layout.tsx @@ -1,6 +1,6 @@ 'use client'; import styles from './layout.module.scss'; -import { useGetMe } from '@/entities/session'; +import { useGetMe, useGetUsers } from '@/entities/session'; import { Header } from './ui/header'; import { Flex, Typography } from '@/shared/ui'; import { List } from './ui/list'; @@ -8,44 +8,52 @@ import { Skeleton } from '@/shared/ui/skeleton/skeleton'; import { About } from './ui/about'; import { ArrowLeftIcon, LogoBig } from '@/shared/assets'; import { useRouter, useParams } from 'next/navigation'; -import { Friends } from './ui/Friends'; +import { Friends } from './ui/friends'; import { Fields } from './ui/fields'; +import { useGetUserByName } from './lib/useGetUserByName'; +import { ProfileContext } from './lib/profile-context'; + export default function Layout() { - const { data: user } = useGetMe(); - const router = useRouter(); + const { data: me } = useGetMe(); const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); - const isMyProfile = user?.username === username; + const router = useRouter(); + const isMyProf = me?.username === username; let body = ; - if (user) { + if (user && me) { body = ( - - - - - - - - + <> +
+ + + + + + + + + - + ); } return ( -
- - - - + +
+ + + + + + {body} -
- {body} - -
+
+ ); } diff --git a/client/src/app/(main)/[username]/profile/lib/profile-context.ts b/client/src/app/(main)/[username]/profile/lib/profile-context.ts new file mode 100644 index 00000000..47d81496 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/lib/profile-context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const ProfileContext = createContext(false); diff --git a/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts new file mode 100644 index 00000000..92e3720c --- /dev/null +++ b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts @@ -0,0 +1,5 @@ +import { useGetUsers } from '@/entities/session'; + +export const useGetUserByName = (username: string) => { + return useGetUsers({ filters: JSON.stringify({ username }) }); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/Friends.tsx b/client/src/app/(main)/[username]/profile/ui/Friends.tsx deleted file mode 100644 index d293850a..00000000 --- a/client/src/app/(main)/[username]/profile/ui/Friends.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useGetMe } from '@/entities/session'; -import { Card } from './card'; - -export const Friends = () => { - const { data: user } = useGetMe(); - - return Team here; -}; diff --git a/client/src/app/(main)/[username]/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about.tsx index d8e602d0..084793be 100644 --- a/client/src/app/(main)/[username]/profile/ui/about.tsx +++ b/client/src/app/(main)/[username]/profile/ui/about.tsx @@ -1,14 +1,15 @@ -import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Flex, Typography } from '@/shared/ui'; import { GithubIcon } from '@/shared/assets/icons/github-icon'; import { BehanceIcon } from '@/shared/assets/icons/behance'; import { TelegramIcon } from '@/shared/assets/icons/telegram'; import { LinkedinIcon } from '@/shared/assets/icons/linkedin'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../lib/useGetUserByName'; export const About = () => { - const { data: user } = useGetMe(); - console.log(user?.links?.github); + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); const linksPresent = user?.links && Object.keys(user.links).length; const descPresent = typeof user?.description === 'string'; diff --git a/client/src/app/(main)/[username]/profile/ui/fields.tsx b/client/src/app/(main)/[username]/profile/ui/fields.tsx index 2106a25e..c9412647 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields.tsx @@ -1,4 +1,3 @@ -import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Flex, Typography } from '@/shared/ui'; import { useState } from 'react'; @@ -6,10 +5,7 @@ import styles from './fields.module.scss'; import { Skills } from './fields/skills'; import { WorkExperience } from './fields/work-experience'; import { Education } from './fields/education'; - export const Fields = () => { - const { data: user } = useGetMe(); - const [field, setField] = useState('Skills'); const fields = { diff --git a/client/src/app/(main)/[username]/profile/ui/fields/education.tsx b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx index 7b3d9e53..2bf19a80 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/education.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx @@ -1,14 +1,13 @@ import { useGetMe } from '@/entities/session'; import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; import { useState } from 'react'; -import styles from './fields.module.scss'; export const Education = () => { const { data: user } = useGetMe(); const universities = user?.universities; if (!universities) return No information; return ( - + {universities.map((education, i) => { const start = new Date(education.admissionDate).getFullYear(); const end = education.graduationDate diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx index 53632a1c..f8dbc0c8 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -1,10 +1,10 @@ -import { useGetMe } from '@/entities/session'; import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; -import { useState } from 'react'; -import styles from './fields.module.scss'; +import { useGetUserByName } from '@/app/(main)/[username]/profile/lib/useGetUserByName'; +import { useParams } from 'next/navigation'; export const Skills = () => { - const { data: user } = useGetMe(); + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); const skills = { programmingLanguages: { @@ -38,14 +38,13 @@ export const Skills = () => { {user?.skills && Object.entries(skills).map(skill => { const skillName = skill[0] as keyof typeof skills; - if (!user.skills[skillName]) return null; const Badge = skills[skillName].Badge; return ( {skills[skillName].title} - {user?.skills && ( + {user?.skills[skillName] && ( - {user.skills[skillName].map((lang: string) => ( + {user!.skills[skillName].map((lang: string) => ( ))} diff --git a/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx b/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx index 65cce1bc..47a0e6d8 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/work-experience.tsx @@ -1,14 +1,11 @@ import { useGetMe } from '@/entities/session'; -import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; -import { useState } from 'react'; -import styles from './fields.module.scss'; - +import { Flex, Typography } from '@/shared/ui'; export const WorkExperience = () => { const { data: user } = useGetMe(); - const jobs = user?.jobs; + const jobs = user.jobs; return ( - - {jobs.map((job, i) => { + + {jobs.map((job, i: number) => { const start = new Date(job.startDate).getFullYear(); const end = job.endDate ? new Date(job.endDate).getFullYear() : 'Present'; return ( diff --git a/client/src/app/(main)/[username]/profile/ui/friends.tsx b/client/src/app/(main)/[username]/profile/ui/friends.tsx new file mode 100644 index 00000000..fd7a5d17 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/friends.tsx @@ -0,0 +1,10 @@ +import { useGetFriends, useGetMe } from '@/entities/session'; +import { Card } from './card'; + +export const Friends = () => { + const { data: user } = useGetMe(); + + const mockFriends = { data: [] }; + + return Friends here; +}; diff --git a/client/src/app/(main)/[username]/profile/ui/header.tsx b/client/src/app/(main)/[username]/profile/ui/header.tsx index b51fe4d5..b93ea3e3 100644 --- a/client/src/app/(main)/[username]/profile/ui/header.tsx +++ b/client/src/app/(main)/[username]/profile/ui/header.tsx @@ -1,16 +1,47 @@ 'use client'; import styles from './header.module.scss'; import { useGetMe } from '@/entities/session'; -import { UserPlusIcon } from '@/shared/assets'; +import { ChatCircleDotsIcon, PlusIcon, UserPlusIcon } from '@/shared/assets'; import { Button, Flex, ImageLoader, Typography } from '@/shared/ui'; import { Skeleton } from '@/shared/ui/skeleton/skeleton'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../lib/useGetUserByName'; +import { useContext } from 'react'; +import { ProfileContext } from '@/app/(main)/[username]/profile/lib/profile-context'; +import { useAddFriend } from '@/entities/session/api/useAddFriend'; export const Header = () => { - const { data: user } = useGetMe(); - if (!user) { + const { username } = useParams(); + const { data: me } = useGetMe(); + const { data: user } = useGetUserByName(username as string); + const isMyProfile = useContext(ProfileContext); + const { mutate: addFriend } = useAddFriend(String(me.id), String(user!.id)); + + if (!user || !me) { return ; } - const username = user.username ? '@' + user.username : ''; + let interactions = ( + + ); + if (!isMyProfile) { + interactions = ( + + {' '} + + + ); + } + + const name = user.username ? '@' + user.username : ''; return (
@@ -31,14 +62,11 @@ export const Header = () => {
{user.fullName}
- {username} + {name}
- + {interactions}
diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx index 9d55ebb8..79e0bc6b 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -5,9 +5,12 @@ import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; import { Row } from './row'; import { Flex } from '@/shared/ui'; import { calculateAge } from '@/shared/lib'; +import { useParams } from 'next/navigation'; +import { useGetUserByName } from '../lib/useGetUserByName'; export const List = () => { - const { data: user } = useGetMe(); + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); let age = ''; if (user?.dateOfBirth) { diff --git a/client/src/entities/session/api/index.ts b/client/src/entities/session/api/index.ts index 839cf975..29e99a91 100644 --- a/client/src/entities/session/api/index.ts +++ b/client/src/entities/session/api/index.ts @@ -1,3 +1,4 @@ +export { useGetFriends } from './useGetFriends'; /* Here will be imports for session hooks */ export { useConfirmEmail } from './useConfirmEmail'; export { useForgotPassword } from './useForgotPassword'; diff --git a/client/src/entities/session/api/useAddFriend.tsx b/client/src/entities/session/api/useAddFriend.tsx new file mode 100644 index 00000000..0f69b0e8 --- /dev/null +++ b/client/src/entities/session/api/useAddFriend.tsx @@ -0,0 +1,17 @@ +import { useMutation } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { toast } from 'sonner'; + +export const useAddFriend = (userId: string, receiverId: string) => { + return useMutation({ + mutationFn: async () => + await API.post(`${API_FRIENDSHIP}/${receiverId}?user={"id":"${userId}"}`), + onSuccess: () => { + toast('Request is sent'); + }, + onError: err => { + toast(`Error occurred: ${err}`); + }, + }); +}; diff --git a/client/src/entities/session/api/useGetFriends.tsx b/client/src/entities/session/api/useGetFriends.tsx new file mode 100644 index 00000000..644428b1 --- /dev/null +++ b/client/src/entities/session/api/useGetFriends.tsx @@ -0,0 +1,19 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; +import { IUserProtectedResponse } from '@teameights/types'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; + +export const useGetFriends = (userId: number) => { + return useQuery({ + queryKey: ['useGetFriends', userId], + queryFn: async () => { + const { data } = await API.get( + `${API_FRIENDSHIP}/${userId}?filters={"status":"accepted"}` + ); + return data; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + }); +}; diff --git a/client/src/entities/session/api/useGetUsers.tsx b/client/src/entities/session/api/useGetUsers.tsx index 81c10796..cc238fff 100644 --- a/client/src/entities/session/api/useGetUsers.tsx +++ b/client/src/entities/session/api/useGetUsers.tsx @@ -22,8 +22,10 @@ export const useGetUsers = ({ page, limit, filters, sort }: IQueryParams) => { return useQuery({ queryKey: ['useGetUsers', queryString], queryFn: async () => { - const { data } = await API.get(`${API_USERS}?${queryString}`); - return data; + const { data } = await API.get<{ data: IUserProtectedResponse[] }>( + `${API_USERS}?${queryString}` + ); + return data.data[0]; }, refetchOnMount: false, refetchOnWindowFocus: false, diff --git a/client/src/shared/constant/server-routes.ts b/client/src/shared/constant/server-routes.ts index 72e46522..2a706c43 100644 --- a/client/src/shared/constant/server-routes.ts +++ b/client/src/shared/constant/server-routes.ts @@ -5,6 +5,7 @@ export const API_EMAIL_CONFIRM = '/auth/email/confirm'; export const API_FORGOT_PASSWORD = '/auth/forgot/password'; export const API_RESET_PASSWORD = '/auth/reset/password'; export const API_ME = '/auth/me'; +export const API_FRIENDSHIP = '/friendship'; export const API_REFRESH = '/auth/refresh'; export const API_LOGOUT = '/auth/logout'; export const API_GOOGLE_LOGIN = '/auth/google/login'; From b8834355e7246eeb084bdd815e44192911d68f5c Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:30:56 +0100 Subject: [PATCH 29/67] refactor: remove eslint errors --- .../app/(main)/[username]/profile/layout.tsx | 2 +- .../profile/lib/useGetUserByName.ts | 2 +- .../profile/ui/fields/education.tsx | 3 +- .../[username]/profile/ui/fields/skills.tsx | 59 +++++++++---------- .../(main)/[username]/profile/ui/friends.tsx | 4 -- .../app/(main)/[username]/profile/ui/list.tsx | 1 - client/src/shared/assets/icons/index.ts | 4 -- 7 files changed, 31 insertions(+), 44 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx index 175135a4..6a4833ab 100644 --- a/client/src/app/(main)/[username]/profile/layout.tsx +++ b/client/src/app/(main)/[username]/profile/layout.tsx @@ -1,6 +1,6 @@ 'use client'; import styles from './layout.module.scss'; -import { useGetMe, useGetUsers } from '@/entities/session'; +import { useGetMe } from '@/entities/session'; import { Header } from './ui/header'; import { CardSkeleton, Flex, Typography } from '@/shared/ui'; import { List } from './ui/list'; diff --git a/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts index 31041fb2..7494344b 100644 --- a/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts +++ b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts @@ -2,7 +2,7 @@ import { useGetUsers } from '@/entities/session'; import { IUserResponse } from '@teameights/types'; export const useGetUserByName = (username: string): { data: IUserResponse | undefined } => { - let users = useGetUsers(JSON.stringify({ username: username })); + const users = useGetUsers(JSON.stringify({ username: username })); return { data: users?.data?.pages[0]?.data[0] ?? undefined }; }; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/education.tsx b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx index 2bf19a80..f5a85c36 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/education.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/education.tsx @@ -1,6 +1,5 @@ import { useGetMe } from '@/entities/session'; -import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; -import { useState } from 'react'; +import { Flex, Typography } from '@/shared/ui'; export const Education = () => { const { data: user } = useGetMe(); diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx index a56561e2..36d33ca2 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -1,40 +1,37 @@ -import { BadgeIcon, BadgeText, Flex, Typography } from '@/shared/ui'; -import { useGetUserByName } from '@/app/(main)/[username]/profile/lib/useGetUserByName'; -import { useParams } from 'next/navigation'; +import { Flex } from '@/shared/ui'; export const Skills = () => { - const { username } = useParams(); - const { data: user } = useGetUserByName(username as string); - const skills = { - programmingLanguages: { - Badge: BadgeIcon, - title: 'Programming Languages', - }, - frameworks: { - Badge: BadgeText, - title: 'Frameworks', - }, - fields: { - Badge: BadgeText, - title: 'Fields', - }, - designerTools: { - Badge: BadgeIcon, - title: 'Fields', - }, - projectManagerTools: { - Badge: BadgeIcon, - title: 'Tools', - }, - methodologies: { - Badge: BadgeText, - title: 'Methodologies', - }, - }; + // const skills = { + // programmingLanguages: { + // Badge: BadgeIcon, + // title: 'Programming Languages', + // }, + // frameworks: { + // Badge: BadgeText, + // title: 'Frameworks', + // }, + // fields: { + // Badge: BadgeText, + // title: 'Fields', + // }, + // designerTools: { + // Badge: BadgeIcon, + // title: 'Fields', + // }, + // projectManagerTools: { + // Badge: BadgeIcon, + // title: 'Tools', + // }, + // methodologies: { + // Badge: BadgeText, + // title: 'Methodologies', + // }, + // }; return ( + Temp fix {/*tODO: ромчик тут надо что то по умнее придумать так как щас coreTools/additionalTools*/} {/*{user?.skills &&*/} diff --git a/client/src/app/(main)/[username]/profile/ui/friends.tsx b/client/src/app/(main)/[username]/profile/ui/friends.tsx index fd7a5d17..6e37dd0d 100644 --- a/client/src/app/(main)/[username]/profile/ui/friends.tsx +++ b/client/src/app/(main)/[username]/profile/ui/friends.tsx @@ -1,10 +1,6 @@ -import { useGetFriends, useGetMe } from '@/entities/session'; import { Card } from './card'; export const Friends = () => { - const { data: user } = useGetMe(); - - const mockFriends = { data: [] }; return Friends here; }; diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx index 79e0bc6b..8d4d0504 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -1,4 +1,3 @@ -import { useGetMe } from '@/entities/session'; import { Card } from './card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; // import { Row } from '@/app/(main)/user/[username]/profile/ui/row'; diff --git a/client/src/shared/assets/icons/index.ts b/client/src/shared/assets/icons/index.ts index 319316aa..5777ed8c 100644 --- a/client/src/shared/assets/icons/index.ts +++ b/client/src/shared/assets/icons/index.ts @@ -1,7 +1,3 @@ -import { GithubIcon } from './github-icon'; -import { BehanceIcon } from './behance'; -import { LinkedinIcon } from './linkedin'; -import { TelegramIcon } from './telegram'; export { Star } from './star'; export { Cake } from './cake'; export { MapPin } from './map-pin'; From 8da9a841f05f7e55402ad3c261853753d4131a98 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:33:23 +0100 Subject: [PATCH 30/67] fix: back icon position --- .../src/app/(main)/[username]/profile/layout.module.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/app/(main)/[username]/profile/layout.module.scss b/client/src/app/(main)/[username]/profile/layout.module.scss index e54bbbf8..f1b621b1 100644 --- a/client/src/app/(main)/[username]/profile/layout.module.scss +++ b/client/src/app/(main)/[username]/profile/layout.module.scss @@ -20,4 +20,11 @@ opacity: .7; transition: .3s; } + @media (max-width: 1127px) { + left: 0; + } + @media (max-width: 790px) { + right: 0; + left: unset; + } } \ No newline at end of file From c714b7234580537739a3aecfde05c2299fe121ee Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:33:11 +0100 Subject: [PATCH 31/67] refactor: run Prettier --- client/src/app/(main)/[username]/profile/ui/fields/skills.tsx | 2 -- client/src/app/(main)/[username]/profile/ui/friends.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx index 36d33ca2..2ed5d666 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -1,7 +1,6 @@ import { Flex } from '@/shared/ui'; export const Skills = () => { - // const skills = { // programmingLanguages: { // Badge: BadgeIcon, @@ -33,7 +32,6 @@ export const Skills = () => { Temp fix {/*tODO: ромчик тут надо что то по умнее придумать так как щас coreTools/additionalTools*/} - {/*{user?.skills &&*/} {/* Object.entries(skills).map(skill => {*/} {/* const skillName = skill[0] as keyof typeof skills;*/} diff --git a/client/src/app/(main)/[username]/profile/ui/friends.tsx b/client/src/app/(main)/[username]/profile/ui/friends.tsx index 6e37dd0d..818711b5 100644 --- a/client/src/app/(main)/[username]/profile/ui/friends.tsx +++ b/client/src/app/(main)/[username]/profile/ui/friends.tsx @@ -1,6 +1,5 @@ import { Card } from './card'; export const Friends = () => { - return Friends here; }; From 7bb4e0ba7b848f7b5819c1c2fdb4fca007c8b114 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Wed, 14 Feb 2024 21:43:13 +0100 Subject: [PATCH 32/67] fix: specialty field to match updated type --- client/src/app/(main)/[username]/profile/ui/list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list.tsx index 8d4d0504..edbd0a7d 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list.tsx @@ -19,7 +19,7 @@ export const List = () => { return ( - } text={user?.speciality ?? ''} /> + } text={user?.skills?.speciality ?? ''} /> } text={user?.experience ?? ''} /> } text={user?.country ?? ''} /> {age && } text={`${age} years old`} />} From 00cec748051b0127256eff9f2b342c847d1cb7ad Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Wed, 14 Feb 2024 22:31:46 +0100 Subject: [PATCH 33/67] fix: skills to match updated structure --- .../(main)/[username]/profile/ui/fields.tsx | 11 +-- .../[username]/profile/ui/fields/skills.tsx | 71 ++++++++----------- .../entities/session/api/useAcceptFriend.tsx | 17 +++++ 3 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 client/src/entities/session/api/useAcceptFriend.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/fields.tsx b/client/src/app/(main)/[username]/profile/ui/fields.tsx index f6814922..01537806 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields.tsx @@ -6,7 +6,7 @@ import { Skills } from './fields/skills'; import { WorkExperience } from './fields/work-experience'; import { Education } from './fields/education'; export const Fields = () => { - const [field, setField] = useState('Skills'); + const [field, setField] = useState('Skills'); const fields = { Skills: , @@ -23,7 +23,11 @@ export const Fields = () => { {Object.keys(fields).map(key => { const classProps = field === key ? { className: styles.selected } : {}; return ( - + + + {friendsList.slice(0, 8).map(friend => ( + + + + ))} + + + ); + } + return ( + + + + Friends + + {friendsContainer} + + + ); }; diff --git a/client/src/app/(main)/layout.module.scss b/client/src/app/(main)/layout.module.scss index d56a3890..13379f66 100644 --- a/client/src/app/(main)/layout.module.scss +++ b/client/src/app/(main)/layout.module.scss @@ -24,4 +24,7 @@ .placeholder { width: 93px; flex-shrink: 0; + @media (max-width: 768px) { + display: none; + } } \ No newline at end of file From c2258fd1f174e730f4bb107aea277136dd6246ce Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sat, 17 Feb 2024 16:36:03 +0100 Subject: [PATCH 40/67] feat: add friends modal --- .../[username]/profile/ui/friends-modal.tsx | 60 +++++++++++++++++++ .../[username]/profile/ui/friends.module.scss | 8 +++ .../(main)/[username]/profile/ui/friends.tsx | 10 +++- .../entities/session/api/useGetFriends.tsx | 11 +++- 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 client/src/app/(main)/[username]/profile/ui/friends-modal.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/friends-modal.tsx b/client/src/app/(main)/[username]/profile/ui/friends-modal.tsx new file mode 100644 index 00000000..f31f8853 --- /dev/null +++ b/client/src/app/(main)/[username]/profile/ui/friends-modal.tsx @@ -0,0 +1,60 @@ +import { Flex, ImageLoader, Typography } from '@/shared/ui'; +import styles from './friends.module.scss'; +import { Modal } from '@/shared/ui'; +import { getCountryFlag } from '@/shared/lib'; +import { IUserBase } from '@teameights/types'; + +interface FriendsModalProps { + friendsList: IUserBase[]; + isFriendsModalOpen: boolean; + setFriendsModal: (state: boolean) => void; +} + +export const FriendsModal = ({ + friendsList, + isFriendsModalOpen, + setFriendsModal, +}: FriendsModalProps) => { + return ( + setFriendsModal(false)} isOpen={isFriendsModalOpen}> + + + Friends + + + {friendsList.map(friend => { + return ( + + + + + + {friend.username ?? 'usernamehey'} + + + + + {friend.skills?.speciality} + + + + ); + })} + + + + ); +}; diff --git a/client/src/app/(main)/[username]/profile/ui/friends.module.scss b/client/src/app/(main)/[username]/profile/ui/friends.module.scss index 539f9c23..6adccb4c 100644 --- a/client/src/app/(main)/[username]/profile/ui/friends.module.scss +++ b/client/src/app/(main)/[username]/profile/ui/friends.module.scss @@ -21,4 +21,12 @@ .friends_label { margin-left: 3px; +} + +.friends_list { + overflow-y: scroll; +} + +.friends_list_item:first-of-type { + padding-top: 10px; } \ No newline at end of file diff --git a/client/src/app/(main)/[username]/profile/ui/friends.tsx b/client/src/app/(main)/[username]/profile/ui/friends.tsx index 2477c45c..494c5979 100644 --- a/client/src/app/(main)/[username]/profile/ui/friends.tsx +++ b/client/src/app/(main)/[username]/profile/ui/friends.tsx @@ -5,12 +5,15 @@ import { useGetUserByName } from '../lib/useGetUserByName'; import { CardSkeleton, Flex, ImageLoader, Typography } from '@/shared/ui'; import styles from './friends.module.scss'; import { ArrowRightIcon } from '@/shared/assets'; +import { useState } from 'react'; +import { FriendsModal } from './friends-modal'; export const Friends = () => { const { username } = useParams(); const { data: user } = useGetUserByName(username as string); const { data: friends } = useGetFriends(user!.id); const friendshipList = friends?.data; + const [isFriendsModalOpen, setFriendsModal] = useState(false); if (!friends || !friendshipList) { return ; @@ -30,12 +33,17 @@ export const Friends = () => { }); friendsContainer = ( + {friendshipList.length} {noun} - - - - - {body} +
+ + + + -
- + {children} +
+
); } diff --git a/client/src/app/(main)/[username]/profile/page.tsx b/client/src/app/(main)/[username]/profile/page.tsx index 7fcbc82f..364cfcd6 100644 --- a/client/src/app/(main)/[username]/profile/page.tsx +++ b/client/src/app/(main)/[username]/profile/page.tsx @@ -1,5 +1,46 @@ 'use client'; +import styles from './layout.module.scss'; +import { useGetMe } from '@/entities/session'; +import { Header } from './ui/header'; +import { CardSkeleton, Flex } from '@/shared/ui'; +import { List } from './ui/list'; +import { About } from './ui/about'; +import { useParams } from 'next/navigation'; +import { Friends } from './ui/friends'; +import { Fields } from './ui/fields'; +import { useGetUserByName } from './lib/useGetUserByName'; +import { ProfileContext } from './lib/profile-context'; export default function Page() { - return <>; + const { data: me } = useGetMe(); + const { username } = useParams(); + const { data: user } = useGetUserByName(username as string); + + const isMyProf = me?.username === username; + + let body = ( + + + + + ); + if (user && me) { + body = ( + <> +
+ + + + + + + + + + + + ); + } + + return {body} ; } From 7b5b29e73624abd0f4ce48c208528b8f22871f06 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:51:36 +0100 Subject: [PATCH 43/67] feat: add friend removal --- .../app/(main)/[username]/profile/page.tsx | 4 +- .../(main)/[username]/profile/ui/header.tsx | 39 ++++++++++++++++--- .../entities/session/api/useRemoveFriend.tsx | 16 ++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 client/src/entities/session/api/useRemoveFriend.tsx diff --git a/client/src/app/(main)/[username]/profile/page.tsx b/client/src/app/(main)/[username]/profile/page.tsx index 364cfcd6..7c0a8fe9 100644 --- a/client/src/app/(main)/[username]/profile/page.tsx +++ b/client/src/app/(main)/[username]/profile/page.tsx @@ -15,7 +15,6 @@ export default function Page() { const { data: me } = useGetMe(); const { username } = useParams(); const { data: user } = useGetUserByName(username as string); - const isMyProf = me?.username === username; let body = ( @@ -24,7 +23,8 @@ export default function Page() { ); - if (user && me) { + + if (user) { body = ( <>
diff --git a/client/src/app/(main)/[username]/profile/ui/header.tsx b/client/src/app/(main)/[username]/profile/ui/header.tsx index bdaf70f4..8b745063 100644 --- a/client/src/app/(main)/[username]/profile/ui/header.tsx +++ b/client/src/app/(main)/[username]/profile/ui/header.tsx @@ -1,6 +1,6 @@ 'use client'; import styles from './header.module.scss'; -import { useGetMe } from '@/entities/session'; +import { useGetFriends, useGetMe } from '@/entities/session'; import { ChatCircleDotsIcon, PlusIcon, UserPlusIcon } from '@/shared/assets'; import { Button, CardSkeleton, Flex, ImageLoader, Typography } from '@/shared/ui'; import { useParams } from 'next/navigation'; @@ -8,14 +8,23 @@ import { useGetUserByName } from '../lib/useGetUserByName'; import { useContext } from 'react'; import { ProfileContext } from '@/app/(main)/[username]/profile/lib/profile-context'; import { useAddFriend } from '@/entities/session/api/useAddFriend'; +import { useRemoveFriend } from '@/entities/session/api/useRemoveFriend'; export const Header = () => { const { username } = useParams(); const { data: me } = useGetMe(); const { data: user } = useGetUserByName(username as string); const isMyProfile = useContext(ProfileContext); const { mutate: addFriend } = useAddFriend(String(me?.id), String(user!.id)); + const { mutate: removeFriend } = useRemoveFriend(String(user!.id)); + const { data: friendships } = useGetFriends(user!.id); - if (!user || !me) { + const isMyFriend = + me && + friendships?.data.some( + friendship => friendship.creator.id === me.id || friendship.receiver.id === me.id + ); + + if (!user) { return ; } @@ -25,21 +34,39 @@ export const Header = () => { ); + + let friendButton = ( + + ); + + if (isMyFriend) { + friendButton = ( + + ); + } + if (!isMyProfile) { interactions = ( {' '} - + {friendButton} ); } + // Prohibit any interactions with a profile if a user is not logged in + if (!me) { + interactions = <>; + } + const name = user.username ? '@' + user.username : ''; return (
diff --git a/client/src/entities/session/api/useRemoveFriend.tsx b/client/src/entities/session/api/useRemoveFriend.tsx new file mode 100644 index 00000000..47fb1031 --- /dev/null +++ b/client/src/entities/session/api/useRemoveFriend.tsx @@ -0,0 +1,16 @@ +import { useMutation } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { toast } from 'sonner'; + +export const useRemoveFriend = (userId: string) => { + return useMutation({ + mutationFn: async () => await API.delete(`${API_FRIENDSHIP}/${userId}`), + onSuccess: () => { + toast('User is removed from the friends list'); + }, + onError: err => { + toast(`Error occurred: ${err}`); + }, + }); +}; From ad20a4604654ec71580981f14b04255061e15975 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:29:43 +0100 Subject: [PATCH 44/67] refactor: remove unused import --- server/src/modules/friendship/friendship.service.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts index b0ee8fc7..7a4c7830 100644 --- a/server/src/modules/friendship/friendship.service.ts +++ b/server/src/modules/friendship/friendship.service.ts @@ -8,10 +8,7 @@ import { EntityCondition } from '../../utils/types/entity-condition.type'; import { NullableType } from '../../utils/types/nullable.type'; import { FriendshipCheckStatusTypes, FriendshipStatusTypes } from './types/friendship.types'; import { UpdateStatusDto } from './dto/update-status.dto'; -import { - FriendRequestNotificationData, - NotificationStatusEnum, -} from '../notifications/types/notification.type'; +import { NotificationStatusEnum } from '../notifications/types/notification.type'; import { IPaginationOptions } from '../../utils/types/pagination-options'; From af6fca32e33435e6f857bba74f87136e86320bd6 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:37:28 +0100 Subject: [PATCH 45/67] fix: end -> flex-end --- client/src/app/(main)/[username]/profile/ui/header.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(main)/[username]/profile/ui/header.module.scss b/client/src/app/(main)/[username]/profile/ui/header.module.scss index 9f7f61b4..d56cc64f 100644 --- a/client/src/app/(main)/[username]/profile/ui/header.module.scss +++ b/client/src/app/(main)/[username]/profile/ui/header.module.scss @@ -23,6 +23,6 @@ .profile { margin-top: -44px; display: flex; - align-items: end; + align-items: flex-end; gap: 24px; } From 7ae3caf8ec8e00547bb738c7a388b0d13aef7dcf Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:44:17 +0100 Subject: [PATCH 46/67] refactor: split ui to folders --- client/src/app/(main)/[username]/profile/page.tsx | 10 +++++----- .../profile/ui/{ => about}/about.module.scss | 0 .../(main)/[username]/profile/ui/{ => about}/about.tsx | 6 +++--- .../[username]/profile/ui/{ => card}/card.module.scss | 0 .../(main)/[username]/profile/ui/{ => card}/card.tsx | 0 .../profile/ui/{ => fields}/fields.module.scss | 0 .../[username]/profile/ui/{ => fields}/fields.tsx | 10 +++++----- .../profile/ui/{ => friends}/friends-modal.tsx | 0 .../profile/ui/{ => friends}/friends.module.scss | 0 .../[username]/profile/ui/{ => friends}/friends.tsx | 6 +++--- .../profile/ui/{ => header}/header.module.scss | 0 .../[username]/profile/ui/{ => header}/header.tsx | 2 +- .../[username]/profile/ui/{ => list}/list.module.scss | 0 .../(main)/[username]/profile/ui/{ => list}/list.tsx | 8 ++++---- .../[username]/profile/ui/{ => row}/row.module.scss | 0 .../app/(main)/[username]/profile/ui/{ => row}/row.tsx | 0 16 files changed, 21 insertions(+), 21 deletions(-) rename client/src/app/(main)/[username]/profile/ui/{ => about}/about.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => about}/about.tsx (92%) rename client/src/app/(main)/[username]/profile/ui/{ => card}/card.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => card}/card.tsx (100%) rename client/src/app/(main)/[username]/profile/ui/{ => fields}/fields.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => fields}/fields.tsx (84%) rename client/src/app/(main)/[username]/profile/ui/{ => friends}/friends-modal.tsx (100%) rename client/src/app/(main)/[username]/profile/ui/{ => friends}/friends.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => friends}/friends.tsx (94%) rename client/src/app/(main)/[username]/profile/ui/{ => header}/header.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => header}/header.tsx (98%) rename client/src/app/(main)/[username]/profile/ui/{ => list}/list.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => list}/list.tsx (82%) rename client/src/app/(main)/[username]/profile/ui/{ => row}/row.module.scss (100%) rename client/src/app/(main)/[username]/profile/ui/{ => row}/row.tsx (100%) diff --git a/client/src/app/(main)/[username]/profile/page.tsx b/client/src/app/(main)/[username]/profile/page.tsx index 7c0a8fe9..f3c5ea28 100644 --- a/client/src/app/(main)/[username]/profile/page.tsx +++ b/client/src/app/(main)/[username]/profile/page.tsx @@ -1,13 +1,13 @@ 'use client'; import styles from './layout.module.scss'; import { useGetMe } from '@/entities/session'; -import { Header } from './ui/header'; +import { Header } from './ui/header/header'; import { CardSkeleton, Flex } from '@/shared/ui'; -import { List } from './ui/list'; -import { About } from './ui/about'; +import { List } from './ui/list/list'; +import { About } from './ui/about/about'; import { useParams } from 'next/navigation'; -import { Friends } from './ui/friends'; -import { Fields } from './ui/fields'; +import { Friends } from './ui/friends/friends'; +import { Fields } from './ui/fields/fields'; import { useGetUserByName } from './lib/useGetUserByName'; import { ProfileContext } from './lib/profile-context'; diff --git a/client/src/app/(main)/[username]/profile/ui/about.module.scss b/client/src/app/(main)/[username]/profile/ui/about/about.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/about.module.scss rename to client/src/app/(main)/[username]/profile/ui/about/about.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/about.tsx b/client/src/app/(main)/[username]/profile/ui/about/about.tsx similarity index 92% rename from client/src/app/(main)/[username]/profile/ui/about.tsx rename to client/src/app/(main)/[username]/profile/ui/about/about.tsx index 7ae6093c..2ee2227a 100644 --- a/client/src/app/(main)/[username]/profile/ui/about.tsx +++ b/client/src/app/(main)/[username]/profile/ui/about/about.tsx @@ -1,12 +1,12 @@ -import { Card } from './card'; +import { Card } from '../card/card'; import { Flex, Typography } from '@/shared/ui'; import { GithubIcon } from '@/shared/assets/icons/github-icon'; import { BehanceIcon } from '@/shared/assets/icons/behance'; import { TelegramIcon } from '@/shared/assets/icons/telegram'; import { LinkedinIcon } from '@/shared/assets/icons/linkedin'; import { useParams } from 'next/navigation'; -import { useGetUserByName } from '../lib/useGetUserByName'; -import styles from '../layout.module.scss'; +import { useGetUserByName } from '../../lib/useGetUserByName'; +import styles from '../../layout.module.scss'; export const About = () => { const { username } = useParams(); diff --git a/client/src/app/(main)/[username]/profile/ui/card.module.scss b/client/src/app/(main)/[username]/profile/ui/card/card.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/card.module.scss rename to client/src/app/(main)/[username]/profile/ui/card/card.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/card.tsx b/client/src/app/(main)/[username]/profile/ui/card/card.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/card.tsx rename to client/src/app/(main)/[username]/profile/ui/card/card.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/fields.module.scss b/client/src/app/(main)/[username]/profile/ui/fields/fields.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/fields.module.scss rename to client/src/app/(main)/[username]/profile/ui/fields/fields.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/fields.tsx b/client/src/app/(main)/[username]/profile/ui/fields/fields.tsx similarity index 84% rename from client/src/app/(main)/[username]/profile/ui/fields.tsx rename to client/src/app/(main)/[username]/profile/ui/fields/fields.tsx index 32c197e3..301cb53d 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/fields.tsx @@ -1,11 +1,11 @@ -import { Card } from './card'; +import { Card } from '../card/card'; import { Flex, Typography } from '@/shared/ui'; import { useState } from 'react'; import styles from './fields.module.scss'; -import { Skills } from './fields/skills'; -import { WorkExperience } from './fields/work-experience'; -import { Education } from './fields/education'; -import layoutStyles from '../layout.module.scss'; +import { Skills } from './skills'; +import { WorkExperience } from './work-experience'; +import { Education } from './education'; +import layoutStyles from '../../layout.module.scss'; export const Fields = () => { const [field, setField] = useState('Skills'); diff --git a/client/src/app/(main)/[username]/profile/ui/friends-modal.tsx b/client/src/app/(main)/[username]/profile/ui/friends/friends-modal.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/friends-modal.tsx rename to client/src/app/(main)/[username]/profile/ui/friends/friends-modal.tsx diff --git a/client/src/app/(main)/[username]/profile/ui/friends.module.scss b/client/src/app/(main)/[username]/profile/ui/friends/friends.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/friends.module.scss rename to client/src/app/(main)/[username]/profile/ui/friends/friends.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/friends.tsx b/client/src/app/(main)/[username]/profile/ui/friends/friends.tsx similarity index 94% rename from client/src/app/(main)/[username]/profile/ui/friends.tsx rename to client/src/app/(main)/[username]/profile/ui/friends/friends.tsx index 46967422..f51abb3e 100644 --- a/client/src/app/(main)/[username]/profile/ui/friends.tsx +++ b/client/src/app/(main)/[username]/profile/ui/friends/friends.tsx @@ -1,12 +1,12 @@ import { useGetFriends } from '@/entities/session'; -import { Card } from './card'; +import { Card } from '../card/card'; import { useParams } from 'next/navigation'; -import { useGetUserByName } from '../lib/useGetUserByName'; +import { useGetUserByName } from '../../lib/useGetUserByName'; import { CardSkeleton, Flex, ImageLoader, Typography } from '@/shared/ui'; import styles from './friends.module.scss'; import { ArrowRightIcon } from '@/shared/assets'; import { useState } from 'react'; -import layoutStyles from '../layout.module.scss'; +import layoutStyles from '../../layout.module.scss'; import { FriendsModal } from './friends-modal'; export const Friends = () => { diff --git a/client/src/app/(main)/[username]/profile/ui/header.module.scss b/client/src/app/(main)/[username]/profile/ui/header/header.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/header.module.scss rename to client/src/app/(main)/[username]/profile/ui/header/header.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/header.tsx b/client/src/app/(main)/[username]/profile/ui/header/header.tsx similarity index 98% rename from client/src/app/(main)/[username]/profile/ui/header.tsx rename to client/src/app/(main)/[username]/profile/ui/header/header.tsx index 8b745063..d21f1ca8 100644 --- a/client/src/app/(main)/[username]/profile/ui/header.tsx +++ b/client/src/app/(main)/[username]/profile/ui/header/header.tsx @@ -4,7 +4,7 @@ import { useGetFriends, useGetMe } from '@/entities/session'; import { ChatCircleDotsIcon, PlusIcon, UserPlusIcon } from '@/shared/assets'; import { Button, CardSkeleton, Flex, ImageLoader, Typography } from '@/shared/ui'; import { useParams } from 'next/navigation'; -import { useGetUserByName } from '../lib/useGetUserByName'; +import { useGetUserByName } from '../../lib/useGetUserByName'; import { useContext } from 'react'; import { ProfileContext } from '@/app/(main)/[username]/profile/lib/profile-context'; import { useAddFriend } from '@/entities/session/api/useAddFriend'; diff --git a/client/src/app/(main)/[username]/profile/ui/list.module.scss b/client/src/app/(main)/[username]/profile/ui/list/list.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/list.module.scss rename to client/src/app/(main)/[username]/profile/ui/list/list.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/list.tsx b/client/src/app/(main)/[username]/profile/ui/list/list.tsx similarity index 82% rename from client/src/app/(main)/[username]/profile/ui/list.tsx rename to client/src/app/(main)/[username]/profile/ui/list/list.tsx index 6ee071f5..3a3ee6db 100644 --- a/client/src/app/(main)/[username]/profile/ui/list.tsx +++ b/client/src/app/(main)/[username]/profile/ui/list/list.tsx @@ -1,12 +1,12 @@ -import { Card } from './card'; +import { Card } from '../card/card'; import { Star, Cake, MapPin, UserIcon } from '@/shared/assets'; // import { Row } from '@/app/(main)/user/[username]/profile/ui/row'; -import { Row } from './row'; +import { Row } from '../row/row'; import { Flex } from '@/shared/ui'; import { calculateAge } from '@/shared/lib'; import { useParams } from 'next/navigation'; -import styles from '../layout.module.scss'; -import { useGetUserByName } from '../lib/useGetUserByName'; +import styles from '../../layout.module.scss'; +import { useGetUserByName } from '../../lib/useGetUserByName'; export const List = () => { const { username } = useParams(); diff --git a/client/src/app/(main)/[username]/profile/ui/row.module.scss b/client/src/app/(main)/[username]/profile/ui/row/row.module.scss similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/row.module.scss rename to client/src/app/(main)/[username]/profile/ui/row/row.module.scss diff --git a/client/src/app/(main)/[username]/profile/ui/row.tsx b/client/src/app/(main)/[username]/profile/ui/row/row.tsx similarity index 100% rename from client/src/app/(main)/[username]/profile/ui/row.tsx rename to client/src/app/(main)/[username]/profile/ui/row/row.tsx From ed44a82af538a16054a2d582e13d9263d2e104dd Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 18 Feb 2024 21:53:20 +0100 Subject: [PATCH 47/67] fix: remove icon overlay --- client/src/shared/ui/image-loader/image-loader.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/shared/ui/image-loader/image-loader.module.scss b/client/src/shared/ui/image-loader/image-loader.module.scss index 4863c1bb..0e23a780 100644 --- a/client/src/shared/ui/image-loader/image-loader.module.scss +++ b/client/src/shared/ui/image-loader/image-loader.module.scss @@ -1,7 +1,7 @@ .crown_container { position: absolute; transform: rotate(30deg) translateX(-15%) translateY(-255%); - z-index: 1000; + z-index: 1; svg { width: 100%; From 640b97213576fc5c342eaa2172544189907bad3d Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 18 Feb 2024 22:11:41 +0100 Subject: [PATCH 48/67] fix: back icon position --- .../[username]/profile/layout.module.scss | 18 ++++++++++-------- .../app/(main)/[username]/profile/layout.tsx | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/layout.module.scss b/client/src/app/(main)/[username]/profile/layout.module.scss index 08ebb7ae..fb40784a 100644 --- a/client/src/app/(main)/[username]/profile/layout.module.scss +++ b/client/src/app/(main)/[username]/profile/layout.module.scss @@ -3,7 +3,10 @@ display: flex; width: 100%; height: 100%; - + position: relative; + @media (max-width: 790px) { + position: unset; + } .body { width: 100%; } @@ -11,8 +14,8 @@ .back { position: absolute; - left: -20px; - top: 0; + + left: 0; display: flex; align-items: center; gap: 6px; @@ -20,11 +23,10 @@ opacity: .7; transition: .3s; } - @media (max-width: 1127px) { - left: 0; - } + @media (max-width: 790px) { - right: 0; + top: 46px; + right: 15px; left: unset; } } @@ -50,4 +52,4 @@ .list_card { width: 40%; gap: 18px; -} \ No newline at end of file +} diff --git a/client/src/app/(main)/[username]/profile/layout.tsx b/client/src/app/(main)/[username]/profile/layout.tsx index ace5b0cd..8d93b810 100644 --- a/client/src/app/(main)/[username]/profile/layout.tsx +++ b/client/src/app/(main)/[username]/profile/layout.tsx @@ -9,7 +9,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { return (
- + ); - let friendButton = ( - - ); - - if (isMyFriend) { - friendButton = ( - - ); - } - if (!isMyProfile) { interactions = ( @@ -57,7 +32,7 @@ export const Header = () => { Message - {friendButton} + ); } diff --git a/client/src/entities/session/api/useAcceptFriend.tsx b/client/src/entities/session/api/useAcceptFriend.tsx deleted file mode 100644 index 87359973..00000000 --- a/client/src/entities/session/api/useAcceptFriend.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import { API } from '@/shared/api'; -import { API_FRIENDSHIP } from '@/shared/constant'; -import { toast } from 'sonner'; - -export const useAcceptFriend = (userId: string, receiverId: string) => { - return useMutation({ - mutationFn: async () => - await API.patch(`${API_FRIENDSHIP}/${receiverId}?user={"id":"${userId}"}`), - onSuccess: () => { - toast('Request is sent'); - }, - onError: err => { - toast(`Error occurred: ${err}`); - }, - }); -}; diff --git a/client/src/entities/session/api/useAddFriend.tsx b/client/src/entities/session/api/useAddFriend.tsx index 0f69b0e8..6d5bb544 100644 --- a/client/src/entities/session/api/useAddFriend.tsx +++ b/client/src/entities/session/api/useAddFriend.tsx @@ -1,16 +1,19 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { API } from '@/shared/api'; import { API_FRIENDSHIP } from '@/shared/constant'; import { toast } from 'sonner'; -export const useAddFriend = (userId: string, receiverId: string) => { +export const useAddFriend = (userId: number | undefined, receiverId: number) => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => await API.post(`${API_FRIENDSHIP}/${receiverId}?user={"id":"${userId}"}`), onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['useGetFriends'] }); toast('Request is sent'); }, onError: err => { + console.log(err); toast(`Error occurred: ${err}`); }, }); diff --git a/client/src/entities/session/api/useGetFriends.tsx b/client/src/entities/session/api/useGetFriends.tsx index c0ea735d..b60c92fe 100644 --- a/client/src/entities/session/api/useGetFriends.tsx +++ b/client/src/entities/session/api/useGetFriends.tsx @@ -16,7 +16,7 @@ export const useGetFriends = (userId: number) => { queryKey: ['useGetFriends', userId], queryFn: async () => { const { data } = await API.get<{ data: Array }>( - `${API_FRIENDSHIP}/${userId}?filters={"status":"accepted"}` + `${API_FRIENDSHIP}/${userId}` ); return data; }, diff --git a/client/src/entities/session/api/useHandleFriendshipRequest.tsx b/client/src/entities/session/api/useHandleFriendshipRequest.tsx new file mode 100644 index 00000000..0d3b2dae --- /dev/null +++ b/client/src/entities/session/api/useHandleFriendshipRequest.tsx @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { API } from '@/shared/api'; +import { API_FRIENDSHIP } from '@/shared/constant'; +import { toast } from 'sonner'; + +export const useHandleFriendshipRequest = ( + userId: number | undefined, + receiverId: number, + status: 'rejected' | 'accepted' +) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => + await API.patch(`${API_FRIENDSHIP}/${receiverId}?user={"id":"${userId}"}`, { + status, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['useGetFriends'] }); + if (status === 'accepted') { + toast('New friend added'); + } else { + toast('Friend request declined'); + } + }, + onError: err => { + toast(`Error occurred: ${err}`); + }, + }); +}; diff --git a/client/src/entities/session/api/useRemoveFriend.tsx b/client/src/entities/session/api/useRemoveFriend.tsx index 47fb1031..16c66b30 100644 --- a/client/src/entities/session/api/useRemoveFriend.tsx +++ b/client/src/entities/session/api/useRemoveFriend.tsx @@ -1,12 +1,14 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { API } from '@/shared/api'; import { API_FRIENDSHIP } from '@/shared/constant'; import { toast } from 'sonner'; -export const useRemoveFriend = (userId: string) => { +export const useRemoveFriend = (userId: number) => { + const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => await API.delete(`${API_FRIENDSHIP}/${userId}`), onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['useGetFriends'] }); toast('User is removed from the friends list'); }, onError: err => { diff --git a/client/src/features/friend-button/friend-button.tsx b/client/src/features/friend-button/friend-button.tsx new file mode 100644 index 00000000..4ed7a638 --- /dev/null +++ b/client/src/features/friend-button/friend-button.tsx @@ -0,0 +1,73 @@ +import { useAddFriend } from '@/entities/session/api/useAddFriend'; +import { useRemoveFriend } from '@/entities/session/api/useRemoveFriend'; +import { useGetFriends } from '@/entities/session'; +import { Button } from '@/shared/ui'; +import { UserPlusIcon } from '@/shared/assets'; +import { useHandleFriendshipRequest } from '@/entities/session/api/useHandleFriendshipRequest'; + +interface FriendButtonProps { + myId?: number; + userId: number; +} + +export const FriendButton = ({ myId, userId }: FriendButtonProps) => { + const { mutate: addFriend } = useAddFriend(myId, userId); + const { mutate: removeFriend } = useRemoveFriend(userId); + const { mutate: declineFriend } = useHandleFriendshipRequest(myId, userId, 'rejected'); + const { mutate: acceptFriend } = useHandleFriendshipRequest(myId, userId, 'accepted'); + const { data: friendships } = useGetFriends(userId); + const isMyProfile = myId === userId; + + if (!myId || isMyProfile) { + return null; // Hide friend button if user not logged in or it's their profile + } + + const ourFriendshipIndex = friendships?.data.findIndex( + friendship => + (friendship.creator.id === myId || friendship.receiver.id === myId) && + friendship.status !== 'rejected' + ); + + if (!ourFriendshipIndex || ourFriendshipIndex === -1) { + return ( + + ); + } + + const ourFriendship = friendships!.data[ourFriendshipIndex]; + + const { status } = ourFriendship; + + const renderFriendButton = (friendshipStatus: string) => { + switch (friendshipStatus) { + case 'accepted': + return ( + + ); + case 'pending': + return ourFriendship.creator.id !== myId ? ( + <> + + + + ) : ( + + ); + default: + return null; + } + }; + + return renderFriendButton(status); +}; diff --git a/client/src/features/friend-button/index.ts b/client/src/features/friend-button/index.ts new file mode 100644 index 00000000..d1e76b3a --- /dev/null +++ b/client/src/features/friend-button/index.ts @@ -0,0 +1 @@ +export { FriendButton } from './friend-button'; From 71b52f538ca8e9d103dd7335adef7be5eb0f62c6 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:26:13 +0100 Subject: [PATCH 56/67] fix: display correct skills --- .../app/(main)/[username]/profile/lib/useGetUserByName.ts | 2 +- .../src/app/(main)/[username]/profile/ui/fields/skills.tsx | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts index 7494344b..759e171d 100644 --- a/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts +++ b/client/src/app/(main)/[username]/profile/lib/useGetUserByName.ts @@ -4,5 +4,5 @@ import { IUserResponse } from '@teameights/types'; export const useGetUserByName = (username: string): { data: IUserResponse | undefined } => { const users = useGetUsers(JSON.stringify({ username: username })); - return { data: users?.data?.pages[0]?.data[0] ?? undefined }; + return { data: users?.data?.pages[0]?.data[0] }; }; diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx index d011f5ac..71577a7b 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -8,7 +8,7 @@ export const Skills = () => { const { data: user } = useGetUserByName(username as string); const skills = { coreTools: { - badge: BadgeIcon, + badge: ( { data }: { data: string }) => , title: 'Core Tools', }, additionalTools: { @@ -16,10 +16,9 @@ export const Skills = () => { title: 'Additional Tools', }, }; - return ( - {user?.skills && + {user!.skills && Object.entries(skills).map(skill => { const skillName = skill[0] as keyof typeof skills; const Badge = skills[skillName].badge; @@ -27,7 +26,7 @@ export const Skills = () => { {skills[skillName].title} - {user?.skills?.coreTools.map((lang: string) => )} + {user?.skills![skillName]?.map((lang: string) => )} ); From ea02298876c4f28cbedd327f0d3d10f537f6f622 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:36:02 +0100 Subject: [PATCH 57/67] refactor: apply prettier --- client/src/app/(main)/[username]/profile/ui/fields/skills.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx index 71577a7b..94b1b5c3 100644 --- a/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx +++ b/client/src/app/(main)/[username]/profile/ui/fields/skills.tsx @@ -8,7 +8,7 @@ export const Skills = () => { const { data: user } = useGetUserByName(username as string); const skills = { coreTools: { - badge: ( { data }: { data: string }) => , + badge: ({ data }: { data: string }) => , title: 'Core Tools', }, additionalTools: { From 4db1416591a1eacb6499666693071340b5281a2e Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:36:44 +0100 Subject: [PATCH 58/67] feat: add friend button (at user modal) --- .../modals/info-modal/user/desktop/desktop.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx index 377d8d71..dfdcf0be 100644 --- a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx +++ b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx @@ -1,13 +1,16 @@ import { Button, Modal, Typography, Flex, ImageLoader } from '@/shared/ui'; import { FC } from 'react'; -import { ArrowRightIcon, UserPlusIcon, ChatCircleDotsIcon } from '@/shared/assets'; +import { ArrowRightIcon, ChatCircleDotsIcon } from '@/shared/assets'; import { calculateAge, getCountryFlag } from '@/shared/lib'; import { InfoModalUserProps } from '../interfaces'; import { IconLayout } from '../ui/icon-layout/icon-layout'; import { TextLayout } from '../ui/text-layout/text-layout'; +import { FriendButton } from '@/features/friend-button'; +import { useGetMe } from '@/entities/session'; export const UserDesktop: FC = ({ user, isOpenModal, handleClose }) => { const age = user?.dateOfBirth ? calculateAge(user.dateOfBirth) : null; + const { data: me } = useGetMe(); return ( <> @@ -54,12 +57,7 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC - { - - } + {user?.id && } ); @@ -45,22 +52,22 @@ export const FriendButton = ({ myId, userId }: FriendButtonProps) => { switch (friendshipStatus) { case 'accepted': return ( - ); case 'pending': return ourFriendship.creator.id !== myId ? ( <> - - ) : ( - ); diff --git a/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx b/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx new file mode 100644 index 00000000..8af5540d --- /dev/null +++ b/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import { Flex } from '@/shared/ui'; +import { LightningIcon } from '@/shared/assets'; +import { getElapsedTime } from '@/shared/lib'; + +import styles from './notification-item.module.scss'; +import { IFriendNotification } from '@teameights/types'; +import { FriendButton } from '@/features/friend-button'; +import { useGetFriends } from '@/entities/session'; + +interface SystemNotificationProps { + notification: IFriendNotification; +} + +/** + * SidebarFriendNotification component renders system notification content. + * + * @component + * @param {Object} props - The properties object. + * @param {SystemNotification} props.notification - The system notification object. + * + * @example + * + */ +export const SidebarFriendNotification: React.FC = props => { + const { notification } = props; + + const myId = notification.receiver.id; + const userId = notification.data.creator.id; + const { data: friendships } = useGetFriends(userId); + + const isPending = friendships?.data.some( + friendship => + (friendship.creator.id === myId || friendship.receiver.id === myId) && + friendship.status === 'pending' + ); + + return ( + <> + +
+ {!notification.read &&
} + +
+ +

+ {notification.data.creator.username} sent you friend request! +

+ {isPending && ( + + + + )} +
+ +

{getElapsedTime(notification.createdAt)}

+ + ); +}; diff --git a/client/src/widgets/sidebar/ui/notification-item/notification-item.module.scss b/client/src/widgets/sidebar/ui/notification-item/notification-item.module.scss index efae86c2..9771bdc3 100644 --- a/client/src/widgets/sidebar/ui/notification-item/notification-item.module.scss +++ b/client/src/widgets/sidebar/ui/notification-item/notification-item.module.scss @@ -83,3 +83,7 @@ opacity: 0.8; } } + +.notification_button { + width: 85px; +} \ No newline at end of file diff --git a/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx b/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx index c2fb2d00..deb7f666 100644 --- a/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx +++ b/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx @@ -4,6 +4,7 @@ import { SidebarSystemNotification } from './system-notification'; import styles from './notification-item.module.scss'; import { NotificationType } from '@teameights/types'; +import { SidebarFriendNotification } from './friend-notification'; export interface NotificationProps { /** @@ -46,6 +47,8 @@ export const SidebarNotificationsItem: React.FC = props => { switch (notification.type) { case 'system': return ; + case 'friend_request': + return ; // case 'team_invite': // return ( // Date: Sun, 3 Mar 2024 16:34:25 +0100 Subject: [PATCH 60/67] feat: add profile link to user modal --- .../modals/info-modal/user/desktop/desktop.tsx | 11 ++++++----- .../widgets/modals/info-modal/user/phone/phone.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx index dfdcf0be..22a7b7f3 100644 --- a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx +++ b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx @@ -64,11 +64,12 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC - - + + + diff --git a/client/src/widgets/modals/info-modal/user/phone/phone.tsx b/client/src/widgets/modals/info-modal/user/phone/phone.tsx index 3222a596..7857bd2e 100644 --- a/client/src/widgets/modals/info-modal/user/phone/phone.tsx +++ b/client/src/widgets/modals/info-modal/user/phone/phone.tsx @@ -26,10 +26,12 @@ export const UserPhone: FC = ({ user, isOpenModal, handleClo Back - + + + From 44f4bf99ad34a60da2ca5cec176fe0e2e4ad202b Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 3 Mar 2024 16:48:02 +0100 Subject: [PATCH 61/67] refactor: add friend notification interface --- client/src/widgets/sidebar/interfaces/index.ts | 1 + .../widgets/sidebar/interfaces/notification.ts | 17 +++++++++++++++++ .../ui/notification-item/notification-item.tsx | 5 +++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 client/src/widgets/sidebar/interfaces/index.ts create mode 100644 client/src/widgets/sidebar/interfaces/notification.ts diff --git a/client/src/widgets/sidebar/interfaces/index.ts b/client/src/widgets/sidebar/interfaces/index.ts new file mode 100644 index 00000000..d9b217ce --- /dev/null +++ b/client/src/widgets/sidebar/interfaces/index.ts @@ -0,0 +1 @@ +export * from './notification'; diff --git a/client/src/widgets/sidebar/interfaces/notification.ts b/client/src/widgets/sidebar/interfaces/notification.ts new file mode 100644 index 00000000..a42e1bf6 --- /dev/null +++ b/client/src/widgets/sidebar/interfaces/notification.ts @@ -0,0 +1,17 @@ +import { IUserResponse, Identifiable, Timestamps } from '@teameights/types'; + +interface INotificationBase extends Identifiable, Timestamps { + receiver: IUserResponse; + type: 'system' | 'friend_request'; + read: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface IFriendNotification extends INotificationBase { + type: 'friend_request'; + data: { + status: string; + creator: IUserResponse; + }; +} diff --git a/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx b/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx index deb7f666..f9694289 100644 --- a/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx +++ b/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx @@ -3,14 +3,15 @@ import React from 'react'; import { SidebarSystemNotification } from './system-notification'; import styles from './notification-item.module.scss'; -import { NotificationType } from '@teameights/types'; import { SidebarFriendNotification } from './friend-notification'; +import { ISystemNotification } from '@teameights/types'; +import { IFriendNotification } from '../../interfaces'; export interface NotificationProps { /** * The notification object. */ - notification: NotificationType; + notification: ISystemNotification | IFriendNotification; /** * A function to close the notifications modal. */ From 88d2ae3459c87ab3d55b9d3dc8c95ed4d1cf00b6 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:39:27 +0100 Subject: [PATCH 62/67] feat: add width prop to friend btn --- client/src/features/friend-button/friend-button.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/src/features/friend-button/friend-button.tsx b/client/src/features/friend-button/friend-button.tsx index 96780823..458de4b1 100644 --- a/client/src/features/friend-button/friend-button.tsx +++ b/client/src/features/friend-button/friend-button.tsx @@ -10,6 +10,7 @@ interface FriendButtonProps { userId: number; short?: boolean; size?: 'm' | 'l' | 's'; + width?: string; } function getText(text: string, short: boolean) { @@ -17,7 +18,7 @@ function getText(text: string, short: boolean) { return text + ' friend'; } -export const FriendButton = ({ myId, userId, short = false, size = 'm' }: FriendButtonProps) => { +export const FriendButton = ({ myId, userId, short = false, size = 'm', width }: FriendButtonProps) => { const { mutate: addFriend } = useAddFriend(myId, userId); const { mutate: removeFriend } = useRemoveFriend(userId); const { mutate: declineFriend } = useHandleFriendshipRequest(myId, userId, 'rejected'); @@ -37,7 +38,7 @@ export const FriendButton = ({ myId, userId, short = false, size = 'm' }: Friend if (ourFriendshipIndex === undefined || ourFriendshipIndex === -1) { return ( - @@ -52,22 +53,22 @@ export const FriendButton = ({ myId, userId, short = false, size = 'm' }: Friend switch (friendshipStatus) { case 'accepted': return ( - ); case 'pending': return ourFriendship.creator.id !== myId ? ( <> - - ) : ( - ); From 9ed1ebd6f5e96246757311809b621a71a30279c6 Mon Sep 17 00:00:00 2001 From: Romas Bitinas <93491714+pupixipup@users.noreply.github.com> Date: Sun, 3 Mar 2024 17:43:15 +0100 Subject: [PATCH 63/67] feat: add friend button to infoModal --- .../src/features/friend-button/friend-button.tsx | 8 +++++++- .../modals/info-modal/user/desktop/desktop.tsx | 5 ++--- .../modals/info-modal/user/phone/phone.tsx | 15 +++++++-------- .../ui/notification-item/friend-notification.tsx | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/client/src/features/friend-button/friend-button.tsx b/client/src/features/friend-button/friend-button.tsx index 458de4b1..19c37cca 100644 --- a/client/src/features/friend-button/friend-button.tsx +++ b/client/src/features/friend-button/friend-button.tsx @@ -18,7 +18,13 @@ function getText(text: string, short: boolean) { return text + ' friend'; } -export const FriendButton = ({ myId, userId, short = false, size = 'm', width }: FriendButtonProps) => { +export const FriendButton = ({ + myId, + userId, + short = false, + size = 'm', + width, +}: FriendButtonProps) => { const { mutate: addFriend } = useAddFriend(myId, userId); const { mutate: removeFriend } = useRemoveFriend(userId); const { mutate: declineFriend } = useHandleFriendshipRequest(myId, userId, 'rejected'); diff --git a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx index 22a7b7f3..103aaf5d 100644 --- a/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx +++ b/client/src/widgets/modals/info-modal/user/desktop/desktop.tsx @@ -56,9 +56,8 @@ export const UserDesktop: FC = ({ user, isOpenModal, handleC - - {user?.id && } - + + {user?.id && } - } + {user?.id && ( + + )} - ); - } - - const ourFriendship = friendships!.data[ourFriendshipIndex]; - - const { status } = ourFriendship; - - const renderFriendButton = (friendshipStatus: string) => { - switch (friendshipStatus) { - case 'accepted': - return ( - + ); + } + case 'requested': { + return ( + + ); + } + case 'toRespond': { + return ( + <> + - ); - case 'pending': - return ourFriendship.creator.id !== myId ? ( - <> - - - - ) : ( - - ); - default: - return null; + + ); } - }; - - return renderFriendButton(status); + case 'friends': { + return ( + + ); + } + default: + return null; + } }; diff --git a/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx b/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx index ad20a3bb..588328fe 100644 --- a/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx +++ b/client/src/widgets/sidebar/ui/notification-item/friend-notification.tsx @@ -7,7 +7,8 @@ import { getElapsedTime } from '@/shared/lib'; import styles from './notification-item.module.scss'; import { IFriendNotification } from '@/widgets/sidebar/interfaces'; import { FriendButton } from '@/features/friend-button'; -import { useGetFriends } from '@/entities/session'; +import { useGetMe } from '@/entities/session'; +import { useGetFriendshipStatus } from '@/entities/session/api/useGetFriendshipStatus'; interface SystemNotificationProps { notification: IFriendNotification; @@ -28,15 +29,11 @@ interface SystemNotificationProps { export const SidebarFriendNotification: React.FC = props => { const { notification } = props; - const myId = notification.receiver.id; + const { data } = useGetMe(); const userId = notification.data.creator.id; - const { data: friendships } = useGetFriends(userId); - const isPending = friendships?.data.some( - friendship => - (friendship.creator.id === myId || friendship.receiver.id === myId) && - friendship.status === 'pending' - ); + const { data: statusResponse } = useGetFriendshipStatus(userId); + const isPending = statusResponse?.status === 'toRespond'; return ( <> @@ -51,7 +48,7 @@ export const SidebarFriendNotification: React.FC = prop

{isPending && ( - + )}
diff --git a/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx b/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx index f9694289..b6aae7e8 100644 --- a/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx +++ b/client/src/widgets/sidebar/ui/notification-item/notification-item.tsx @@ -5,7 +5,7 @@ import { SidebarSystemNotification } from './system-notification'; import styles from './notification-item.module.scss'; import { SidebarFriendNotification } from './friend-notification'; import { ISystemNotification } from '@teameights/types'; -import { IFriendNotification } from '../../interfaces'; +import { IFriendNotification } from '@/widgets/sidebar/interfaces'; export interface NotificationProps { /** From 169012dc9ef9a2f344f637091421f35911513b8a Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 4 Mar 2024 02:35:46 +0200 Subject: [PATCH 65/67] fix: fixed friendship status receiving --- .../modules/friendship/friendship.service.ts | 64 +++++++------------ 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts index 7a4c7830..94d2aed1 100644 --- a/server/src/modules/friendship/friendship.service.ts +++ b/server/src/modules/friendship/friendship.service.ts @@ -231,7 +231,6 @@ export class FriendshipService { } const receiver = await this.usersService.findOne({ id: receiverId }); - if (!receiver) { throw new HttpException( { @@ -260,27 +259,17 @@ export class FriendshipService { HttpStatus.NOT_FOUND ); } - // const notification = await this.notificationsService.findOne({ - // receiver: { id: receiverId }, - // data: { status: NotificationStatusEnum.pending, creator: creator.toJSON() }, - // }); - // if (!notification) { - // throw new HttpException( - // { - // status: HttpStatus.NOT_FOUND, - // errors: { - // notification: `notification not found`, - // }, - // }, - // HttpStatus.NOT_FOUND - // ); - // } - // (notification.data as FriendRequestNotificationData).status = - // NotificationStatusEnum[dto.status]; - // await this.notificationsService.save(notification); + const notification = await this.notificationsService.findOne({ + receiver: { id: receiverId }, + data: { status: NotificationStatusEnum.pending, creator: creator.toJSON() }, + }); + if (notification) { + await this.notificationsService.deleteNotification(notification); + } friendship.status = FriendshipStatusTypes[dto.status]; await this.friendshipRepository.save(friendship); + console.log(receiver.username); if (dto.status === 'accepted') { await this.notificationsService.createNotification( { @@ -497,41 +486,34 @@ export class FriendshipService { } async getStatus(userId: number, friendId: number) { - const friendshipFrom = await this.findOne({ + const friendshipFromMe = await this.findOne({ creator: { id: userId }, receiver: { id: friendId }, }); - if (friendshipFrom && friendshipFrom.status === FriendshipStatusTypes.accepted) { - return { - status: FriendshipCheckStatusTypes.friends, - }; - } else if ( - friendshipFrom && - (friendshipFrom.status === FriendshipStatusTypes.pending || - friendshipFrom.status === FriendshipStatusTypes.rejected) - ) { - return { - status: FriendshipCheckStatusTypes.requested, - }; - } - const friendshipTo = await this.findOne({ + const friendshipToMe = await this.findOne({ creator: { id: friendId }, receiver: { id: userId }, }); - if (friendshipTo && friendshipTo.status === FriendshipStatusTypes.accepted) { + if ( + (friendshipFromMe && friendshipFromMe.status === FriendshipStatusTypes.accepted) || + (friendshipToMe && friendshipToMe.status === FriendshipStatusTypes.accepted) + ) { return { status: FriendshipCheckStatusTypes.friends, }; - } else if (friendshipTo && friendshipTo.status === FriendshipStatusTypes.pending) { + } else if (friendshipToMe && friendshipToMe.status === FriendshipStatusTypes.pending) { return { status: FriendshipCheckStatusTypes.toRespond, }; - } - - if ( - (!friendshipTo && !friendshipFrom) || - (friendshipTo && friendshipTo.status === FriendshipStatusTypes.rejected) + } else if ( + friendshipFromMe && + (friendshipFromMe.status === FriendshipStatusTypes.pending || + friendshipFromMe.status === FriendshipStatusTypes.rejected) ) { + return { + status: FriendshipCheckStatusTypes.requested, + }; + } else { return { status: FriendshipCheckStatusTypes.none, }; From da969bb9f8314167f1ef9ec3466b614302093105 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 4 Mar 2024 02:39:58 +0200 Subject: [PATCH 66/67] fix: cleanup --- server/src/modules/friendship/friendship.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/modules/friendship/friendship.service.ts b/server/src/modules/friendship/friendship.service.ts index 94d2aed1..28b2b780 100644 --- a/server/src/modules/friendship/friendship.service.ts +++ b/server/src/modules/friendship/friendship.service.ts @@ -269,7 +269,6 @@ export class FriendshipService { friendship.status = FriendshipStatusTypes[dto.status]; await this.friendshipRepository.save(friendship); - console.log(receiver.username); if (dto.status === 'accepted') { await this.notificationsService.createNotification( { From 7a995152c9523bb0fc7d1b82809c5c1a5677a0b2 Mon Sep 17 00:00:00 2001 From: Nikita Mashchenko Date: Sat, 9 Mar 2024 12:44:17 -0800 Subject: [PATCH 67/67] fix: generation --- server/test/user/users.e2e-spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/test/user/users.e2e-spec.ts b/server/test/user/users.e2e-spec.ts index fb6155c3..94ff65fa 100644 --- a/server/test/user/users.e2e-spec.ts +++ b/server/test/user/users.e2e-spec.ts @@ -6,7 +6,10 @@ describe('Get users (e2e)', () => { const app = APP_URL; const newUserPassword = `secret`; const fullName = 'Slavik Ukraincev'; - const username = faker.internet.userName().toLowerCase(); + const username = faker.internet + .userName() + .toLowerCase() + .replace(/[^a-z0-9]/gi, ''); const country = 'Ukraine'; const speciality = 'Developer'; const focus = 'Backend Developer';