diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts index cd9ac38..53ee2a8 100644 --- a/apps/backend/src/app.controller.ts +++ b/apps/backend/src/app.controller.ts @@ -13,10 +13,16 @@ import { IssuesService } from './issues/issues.service'; import { IssueType } from './issues/entities/issue-type.enum'; import { CreateIssueDto } from './issues/dto/create-issue.dto'; import { CreateIssueCommentDto } from './issues/dto/create-issue-comment.dto'; +import { SolutionsService } from './solutions/solutions.service'; +import { CreateSolutionDto } from './solutions/dto/create-solution.dto'; +import { CreateSolutionCommentDto } from './solutions/dto/create-solution-comment.dto'; @Controller() export class AppController { - constructor(private readonly issues: IssuesService) {} + constructor( + private readonly issues: IssuesService, + private readonly solutions: SolutionsService, + ) {} @Get('/issues/problems') listProblems() { @@ -35,13 +41,27 @@ export class AppController { @Get('/issues/:id') async getIssue(@Param('id', ParseUUIDPipe) id: string) { const data = await this.issues.findOne(id).then((i) => i.toDto()); - const comments = await this.issues.getCommentsForIssue(id); + const comments = await this.issues + .getCommentsForIssue(id) + .then((c) => c.map((c) => c.toDto())); + const solution = + (await this.solutions.getForIssue(id).catch(() => undefined))?.toList() ?? + undefined; + return { data, comments, solution }; + } + + @Get('/issues/:id/solution') + async getSolution(@Param('id', ParseUUIDPipe) id: string) { + const data = await this.solutions.getForIssue(id).then((i) => i.toDto()); + const comments = await this.solutions + .getComments(id) + .then((c) => c.map((c) => c.toDto())); return { data, comments }; } @Post('/issues') createIssue(@Body() dto: CreateIssueDto) { - return this.issues.create(dto); + return this.issues.create(dto).then((i) => i.toDto()); } @Post('/issues/:id/comments') @@ -52,6 +72,22 @@ export class AppController { return this.issues.addComment(id, dto).then((c) => c.toDto()); } + @Post('/issues/:id/solution') + createSolution( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: CreateSolutionDto, + ) { + return this.solutions.create(id, dto).then((r) => r.toDto()); + } + + @Post('/issues/:id/solution/comments') + addSolutionComment( + @Body() dto: CreateSolutionCommentDto, + @Param('id', ParseUUIDPipe) id: string, + ) { + return this.solutions.addComment(id, dto).then((c) => c.toDto()); + } + @Post('/issues/:id/like') @HttpCode(HttpStatus.ACCEPTED) async addLike(@Param('id', ParseUUIDPipe) id: string) { @@ -64,6 +100,18 @@ export class AppController { await this.issues.dislike(id); } + @Post('/issues/:id/solution/like') + @HttpCode(HttpStatus.ACCEPTED) + async addLikeToSolution(@Param('id', ParseUUIDPipe) id: string) { + await this.solutions.like(id); + } + + @Post('/issues/:id/solution/dislike') + @HttpCode(HttpStatus.ACCEPTED) + async addDislikeToSolution(@Param('id', ParseUUIDPipe) id: string) { + await this.solutions.dislike(id); + } + @Delete('/issues/:id/like') @HttpCode(HttpStatus.ACCEPTED) async removeLike(@Param('id', ParseUUIDPipe) id: string) { @@ -75,4 +123,16 @@ export class AppController { async removeDislike(@Param('id', ParseUUIDPipe) id: string) { await this.issues.dislike(id, true); } + + @Delete('/issues/:id/solution/like') + @HttpCode(HttpStatus.ACCEPTED) + async removeLikeToSolution(@Param('id', ParseUUIDPipe) id: string) { + await this.solutions.like(id, true); + } + + @Delete('/issues/:id/solution/dislike') + @HttpCode(HttpStatus.ACCEPTED) + async removeDislikeToSolution(@Param('id', ParseUUIDPipe) id: string) { + await this.solutions.dislike(id, true); + } } diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index fbe3c89..da2bb68 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IssuesModule } from './issues/issues.module'; import { AppController } from './app.controller'; +import { SolutionsModule } from './solutions/solutions.module'; @Module({ controllers: [AppController], @@ -17,6 +18,7 @@ import { AppController } from './app.controller'; synchronize: process.env.NODE_ENV !== 'production', }), IssuesModule, + SolutionsModule, ], }) export class AppModule {} diff --git a/apps/backend/src/issues/issues.service.ts b/apps/backend/src/issues/issues.service.ts index 39066f5..f37b910 100644 --- a/apps/backend/src/issues/issues.service.ts +++ b/apps/backend/src/issues/issues.service.ts @@ -20,9 +20,9 @@ export class IssuesService { return this.repository.save(issue); } - async addComment(id: string, { content }: CreateIssueCommentDto) { + async addComment(id: string, dto: CreateIssueCommentDto) { const issue = await this.findOne(id); - const comment = this.comments.create({ content }); + const comment = this.comments.create(dto); comment.issue = Promise.resolve(issue); return this.comments.save(comment); } diff --git a/apps/backend/src/solutions/dto/create-solution-comment.dto.ts b/apps/backend/src/solutions/dto/create-solution-comment.dto.ts new file mode 100644 index 0000000..a36e3c7 --- /dev/null +++ b/apps/backend/src/solutions/dto/create-solution-comment.dto.ts @@ -0,0 +1,13 @@ +import { IsString, MaxLength, MinLength } from 'class-validator'; + +export class CreateSolutionCommentDto { + @IsString() + @MinLength(3) + @MaxLength(16) + username: string; + + @IsString() + @MinLength(1) + @MaxLength(255) + content: string; +} diff --git a/apps/backend/src/solutions/dto/create-solution.dto.ts b/apps/backend/src/solutions/dto/create-solution.dto.ts new file mode 100644 index 0000000..aef3ca7 --- /dev/null +++ b/apps/backend/src/solutions/dto/create-solution.dto.ts @@ -0,0 +1,13 @@ +import { IsString, MaxLength, MinLength } from 'class-validator'; + +export class CreateSolutionDto { + @IsString() + @MinLength(1) + @MaxLength(32) + title: string; + + @IsString() + @MinLength(1) + @MaxLength(255) + description: string; +} diff --git a/apps/backend/src/solutions/entities/solution-comment.entity.ts b/apps/backend/src/solutions/entities/solution-comment.entity.ts new file mode 100644 index 0000000..6037e70 --- /dev/null +++ b/apps/backend/src/solutions/entities/solution-comment.entity.ts @@ -0,0 +1,34 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Solution } from './solution.entity'; + +@Entity('solution_comments') +export class SolutionComment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + username: string; + + @Column() + content: string; + + @CreateDateColumn() + createdAt: Date; + + @ManyToOne(() => Solution, { lazy: true, onDelete: 'CASCADE' }) + solution: Promise; + + toDto() { + return { + username: this.username, + content: this.content, + createdAt: this.createdAt.toISOString(), + }; + } +} diff --git a/apps/backend/src/solutions/entities/solution.entity.ts b/apps/backend/src/solutions/entities/solution.entity.ts new file mode 100644 index 0000000..e617701 --- /dev/null +++ b/apps/backend/src/solutions/entities/solution.entity.ts @@ -0,0 +1,53 @@ +import { Issue } from 'src/issues/entities/issue.entity'; +import { + Column, + CreateDateColumn, + Entity, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity('solutions') +export class Solution { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + title: string; + + @Column() + username: string; + + @Column() + description: string; + + @CreateDateColumn() + createdAt: Date; + + @OneToOne(() => Issue, { lazy: true, onDelete: 'CASCADE' }) + issue: Promise; + + @Column({ default: 0 }) + likes: number; + + @Column({ default: 0 }) + dislikes: number; + + async toDto() { + return { + ...this.toList(), + description: this.description, + issue: (await this.issue).toList(), + }; + } + + toList() { + return { + title: this.title, + username: this.username, + likes: this.likes, + dislikes: this.dislikes, + createdAt: this.createdAt.toISOString(), + }; + } +} diff --git a/apps/backend/src/solutions/solutions.module.ts b/apps/backend/src/solutions/solutions.module.ts new file mode 100644 index 0000000..8a2f58d --- /dev/null +++ b/apps/backend/src/solutions/solutions.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { SolutionsService } from './solutions.service'; +import { IssuesModule } from 'src/issues/issues.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Solution } from './entities/solution.entity'; +import { SolutionComment } from './entities/solution-comment.entity'; + +@Module({ + imports: [ + IssuesModule, + TypeOrmModule.forFeature([Solution, SolutionComment]), + ], + providers: [SolutionsService], + exports: [SolutionsService], +}) +export class SolutionsModule {} diff --git a/apps/backend/src/solutions/solutions.service.ts b/apps/backend/src/solutions/solutions.service.ts new file mode 100644 index 0000000..0369f11 --- /dev/null +++ b/apps/backend/src/solutions/solutions.service.ts @@ -0,0 +1,66 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { CreateSolutionDto } from './dto/create-solution.dto'; +import { IssuesService } from 'src/issues/issues.service'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Solution } from './entities/solution.entity'; +import { Repository } from 'typeorm'; +import { CreateSolutionCommentDto } from './dto/create-solution-comment.dto'; +import { SolutionComment } from './entities/solution-comment.entity'; + +@Injectable() +export class SolutionsService { + constructor( + @InjectRepository(Solution) + private readonly repository: Repository, + @InjectRepository(SolutionComment) + private readonly comments: Repository, + private readonly issues: IssuesService, + ) {} + + async create(issueId: string, createSolutionDto: CreateSolutionDto) { + if ((await this.getForIssue(issueId)) !== null) + throw new ConflictException('This issue already has a solution'); + const solution = this.repository.create({ ...createSolutionDto }); + const issue = await this.issues.findOne(issueId); + solution.issue = Promise.resolve(issue); + return this.repository.save(solution); + } + + async getForIssue(issueId: string): Promise { + const ent = await this.repository.findOneBy({ issue: { id: issueId } }); + if (ent === null) { + throw new NotFoundException( + 'Solution for the given issue has not been found', + ); + } + + return ent; + } + + async like(issueId: string, remove?: boolean) { + const ent = await this.getForIssue(issueId); + ent.likes = remove ? ent.likes - 1 : ent.likes + 1; + return this.repository.save(ent); + } + + async dislike(issueId: string, remove?: boolean) { + const ent = await this.getForIssue(issueId); + ent.dislikes = remove ? ent.dislikes - 1 : ent.dislikes + 1; + return this.repository.save(ent); + } + + async addComment(issueId: string, dto: CreateSolutionCommentDto) { + const ent = await this.getForIssue(issueId); + const comment = this.comments.create(dto); + comment.solution = Promise.resolve(ent); + return this.comments.save(comment); + } + + async getComments(id: string) { + return this.comments.findBy({ solution: { id } }); + } +} diff --git a/apps/frontend/src/app/models/solution.model.ts b/apps/frontend/src/app/models/solution.model.ts index 8be7fe1..cbcaa69 100644 --- a/apps/frontend/src/app/models/solution.model.ts +++ b/apps/frontend/src/app/models/solution.model.ts @@ -1,6 +1,8 @@ import { CommentModel } from './comment.model'; export interface SolutionModel { + title: string; + description: string; comments: CommentModel[]; - id: string; + createdAt: Date; } diff --git a/apps/frontend/src/app/modules/solutions/solutions.component.html b/apps/frontend/src/app/modules/solutions/solutions.component.html index c418c67..90e3e1e 100644 --- a/apps/frontend/src/app/modules/solutions/solutions.component.html +++ b/apps/frontend/src/app/modules/solutions/solutions.component.html @@ -1 +1,4 @@ -

solutions works!

+
+ {{solution.title}} +
+gv gvghv \ No newline at end of file diff --git a/apps/frontend/src/app/modules/solutions/solutions.component.ts b/apps/frontend/src/app/modules/solutions/solutions.component.ts index 906aff9..c774346 100644 --- a/apps/frontend/src/app/modules/solutions/solutions.component.ts +++ b/apps/frontend/src/app/modules/solutions/solutions.component.ts @@ -1,10 +1,22 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { EMPTY, Observable } from 'rxjs'; +import { SolutionModel } from 'src/app/models/solution.model'; +import { receiveAllIssuesAction } from 'src/app/store/actions/issue.actions'; +import { selectAllSolutions } from 'src/app/store/selectors/issue.selectors'; +import { SolutionsModule } from './solutions.module'; @Component({ selector: 'app-solutions', templateUrl: './solutions.component.html', styleUrls: ['./solutions.component.scss'] }) -export class SolutionsComponent { - +export class SolutionsComponent implements OnInit { + solutions$: Observable = EMPTY + constructor(private store: Store){} + ngOnInit(): void { + this.store.dispatch(receiveAllIssuesAction()) + this.solutions$ = this.store.select(selectAllSolutions) + + } } diff --git a/apps/frontend/src/app/services/solutions.service.ts b/apps/frontend/src/app/services/solutions.service.ts new file mode 100644 index 0000000..1af2224 --- /dev/null +++ b/apps/frontend/src/app/services/solutions.service.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map } from 'rxjs'; +import { SolutionModel } from '../models/solution.model'; +import { CommentModel } from '../models/comment.model'; + +@Injectable({ + providedIn: 'root', +}) +export class IssueService { + constructor(private http: HttpClient) {} + + getSolutions(id: string) { + return this.http.get('/issue/'+id+'/solution').pipe( + map((solution) => + solution.map((solution) => ({ + title: solution.title, + description: solution.description, + comments: solution.comments, + createdAt: solution.createdAt + })) + ) + ); + } +} + +interface SolutionDto { + title: string, + description: string, + comments: CommentModel[], + createdAt: Date +} diff --git a/apps/frontend/src/app/store/reducer/issue.reducer.ts b/apps/frontend/src/app/store/reducer/issue.reducer.ts index 154822b..bb60a9b 100644 --- a/apps/frontend/src/app/store/reducer/issue.reducer.ts +++ b/apps/frontend/src/app/store/reducer/issue.reducer.ts @@ -35,6 +35,12 @@ const initialState: IssueState = { photos: [], status: 'new', type: 'problem', + solution: { + comments: [], + createdAt: new Date(), + description: "eoooo", + title: "title" + }, comments: [], }, { diff --git a/apps/frontend/src/app/store/selectors/issue.selectors.ts b/apps/frontend/src/app/store/selectors/issue.selectors.ts index 2a566fc..878c4ba 100644 --- a/apps/frontend/src/app/store/selectors/issue.selectors.ts +++ b/apps/frontend/src/app/store/selectors/issue.selectors.ts @@ -1,5 +1,7 @@ import { createFeatureSelector, createSelector } from '@ngrx/store'; import { IssueState } from '../state/issue.state'; +import { SolutionModel } from 'src/app/models/solution.model'; +import { IssueModel } from 'src/app/models/issue.model'; const selectIssueFeatureState = createFeatureSelector('issueState'); @@ -12,3 +14,5 @@ export const selectIssue = (id: string) => createSelector(selectAllIssues, (issues) => issues.find((issue) => issue.id === id) ); + + export const selectAllSolutions = createSelector(selectAllIssues, issues => issues.map(issue => issue.solution).filter(solution => solution !== undefined) as SolutionModel[]) \ No newline at end of file