Ignore:
Timestamp:
10/31/21 13:26:57 (3 years ago)
Author:
Стојков Марко <mst@…>
Branches:
dev
Children:
728eb31
Parents:
ad079e5
Message:

Vote functionality on frontend

Location:
src/Clients/Angular/finki-chattery/src
Files:
1 added
11 edited
1 moved

Legend:

Unmodified
Added
Removed
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-facade.service.ts

    rad079e5 r7c3e6a8  
    22import { Injectable } from '@angular/core';
    33import { Action, Store } from '@ngrx/store';
    4 import { Observable, Subject, throwError } from 'rxjs';
    5 import { catchError, filter, map } from 'rxjs/operators';
     4import { Observable, Subject } from 'rxjs';
     5import { filter } from 'rxjs/operators';
    66
    77import {
     
    99  PreviewQuestionViewModel,
    1010  QuestionStateViewModel,
    11   SearchQuestionsQueryViewModel
     11  SearchQuestionsQueryViewModel,
     12  VoteType
    1213} from 'src/app/shared-app/models';
    1314import {
     
    1617  GetPreviewQuestionsPopular,
    1718  GetQuestionState,
    18   GetSearchQuestions
     19  GetSearchQuestions,
     20  VoteAnswer
    1921} from './question-state/question.actions';
    2022import { questionStateQuery } from './question-state/question.selectors';
     
    3032
    3133  constructor(private store: Store<QuestionState>) {
    32     this.effectWorking$ = this.store.select(questionStateQuery.effectWorking).pipe(
    33       filter((effect) => effect !== null),
    34       map((effect) => {
    35         if (effect instanceof HttpErrorResponse) {
    36           throw effect;
    37         } else {
    38           return effect;
    39         }
    40       }),
    41       catchError((err) => {
    42         return throwError(err);
    43       })
    44     );
     34    this.effectWorking$ = this.store.select(questionStateQuery.effectWorking).pipe(filter((effect) => effect !== null));
    4535  }
    4636
     
    8171  }
    8272
     73  public voteAnswer(answerUid: string, questionUid: string, voteType: VoteType): void {
     74    this.dispatchEffect(new VoteAnswer(questionUid, answerUid, voteType));
     75  }
     76
    8377  public fetchQuestion(questionUid: string): void {
    8478    this.dispatchEffect(new GetQuestionState(questionUid));
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question-state-response.models.ts

    rad079e5 r7c3e6a8  
     1import { VoteType } from 'src/app/shared-app/models';
     2
    13export class QuestionStateResponse {
    24  public uid!: string;
     
    7375  public text!: string;
    7476}
     77
     78export class VoteAnswerResponse {
     79  public answerUid!: string;
     80  public voteUid!: string;
     81  public voteType!: VoteType;
     82}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.actions.ts

    rad079e5 r7c3e6a8  
    22import { Action } from '@ngrx/store';
    33
    4 import { PreviewQuestionViewModel, QuestionStateViewModel, SearchQuestionsQueryViewModel } from 'src/app/shared-app/models';
     4import {
     5  PreviewQuestionViewModel,
     6  QuestionStateViewModel,
     7  SearchQuestionsQueryViewModel,
     8  VoteAnswerViewModel,
     9  VoteType
     10} from 'src/app/shared-app/models';
    511
    612export enum QuestionActionTypes {
     
    1319  GetSearchQuestions = '[Question] Get search questions',
    1420  GetSearchQuestionsSuccess = '[Question] Get search questions Success',
     21  VoteAnswer = '[Question] Vote answer',
     22  VoteAnswerSuccess = '[Question] Vote answer Success',
    1523  EffectStartedWorking = '[Question] Effect Started Working',
    1624  EffectFinishedWorking = '[Question] Effect Finished Working',
     
    6674}
    6775
     76export class VoteAnswer implements Action {
     77  readonly type = QuestionActionTypes.VoteAnswer;
     78
     79  constructor(public questionUid: string, public answerUid: string, public voteType: VoteType) {}
     80}
     81
     82export class VoteAnswerSuccess implements Action {
     83  readonly type = QuestionActionTypes.VoteAnswerSuccess;
     84
     85  constructor(public payload: VoteAnswerViewModel) {}
     86}
     87
    6888export class EffectStartedWorking implements Action {
    6989  readonly type = QuestionActionTypes.EffectStartedWorking;
     
    89109  | GetPreviewQuestionsPopularSuccess
    90110  | GetSearchQuestionsSuccess
     111  | VoteAnswerSuccess
    91112  | EffectStartedWorking
    92113  | EffectFinishedWorking
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.effects.ts

    rad079e5 r7c3e6a8  
    77import { BaseApiService } from 'src/app/shared-app/services/base-api.service';
    88import { QuestionFacadeService } from '../question-facade.service';
    9 import { PreviewQuestionResponse, QuestionStateResponse } from './question-state.models';
     9import { VoteAnswerRequest } from './question-state-request.models';
     10import { PreviewQuestionResponse, QuestionStateResponse, VoteAnswerResponse } from './question-state-response.models';
    1011import {
    1112  EffectFinishedWorking,
     
    1920  GetSearchQuestions,
    2021  GetSearchQuestionsSuccess,
    21   QuestionActionTypes
     22  QuestionActionTypes,
     23  VoteAnswer,
     24  VoteAnswerSuccess
    2225} from './question.actions';
    2326import { QuestionMapper } from './question.mapper';
     
    103106    );
    104107  });
     108
     109  voteAnswer$ = createEffect(() => {
     110    return this.actions$.pipe(
     111      ofType<VoteAnswer>(QuestionActionTypes.VoteAnswer),
     112      mergeMap((action) => {
     113        const body = new VoteAnswerRequest(action.voteType);
     114        return this.api.post<VoteAnswerResponse>(`v1/questions/${action.questionUid}/answers/${action.answerUid}/votes`, body).pipe(
     115          switchMap((state) => [new VoteAnswerSuccess(QuestionMapper.ToVoteAnswerViewModel(state)), new EffectFinishedWorking()]),
     116          catchError((err) => [new EffectFinishedWorkingError(err)])
     117        );
     118      })
     119    );
     120  });
    105121}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.mapper.ts

    rad079e5 r7c3e6a8  
    1010  QuestionStateViewModel,
    1111  StudentQuestionStateViewModel,
    12   TeamQuestionStateViewModel
     12  TeamQuestionStateViewModel,
     13  VoteAnswerViewModel
    1314} from 'src/app/shared-app/models';
    1415import { TranslateFromJsonService } from 'src/app/shared-app/services';
    15 import { PreviewQuestionResponse, QuestionStateResponse } from './question-state.models';
     16import { PreviewQuestionResponse, QuestionStateResponse, VoteAnswerResponse } from './question-state-response.models';
    1617
    1718export class QuestionMapper {
     
    113114    return questions;
    114115  }
     116
     117  public static ToVoteAnswerViewModel(response: VoteAnswerResponse): VoteAnswerViewModel {
     118    return new VoteAnswerViewModel(response.answerUid, response.voteUid, response.voteType);
     119  }
    115120}
  • src/Clients/Angular/finki-chattery/src/app/core/state/question-state/question.reducers.ts

    rad079e5 r7c3e6a8  
     1import { VoteType } from 'src/app/shared-app/models';
    12import { QuestionAction, QuestionActionTypes } from './question.actions';
    23import { initialState, QuestionState } from './question.state';
     
    2526        searchQuestionsQuery: action.query
    2627      };
     28    case QuestionActionTypes.VoteAnswerSuccess: {
     29      if (state.question) {
     30        return {
     31          ...state,
     32          question: {
     33            ...state.question,
     34            answers: state.question.answers.map((x) => {
     35              if (x.uid === action.payload.answerUid) {
     36                let votesCountNew = x.votesCount;
     37
     38                switch (action.payload.voteType) {
     39                  case VoteType.Upvote:
     40                    votesCountNew++;
     41                    break;
     42                  case VoteType.Downvote:
     43                    votesCountNew--;
     44                    break;
     45                }
     46
     47                return {
     48                  ...x,
     49                  votesCount: votesCountNew
     50                };
     51              }
     52
     53              return x;
     54            })
     55          }
     56        };
     57      }
     58
     59      return {
     60        ...state
     61      };
     62    }
    2763    case QuestionActionTypes.EffectStartedWorking: {
    2864      return {
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/generic/vote/vote.component.ts

    rad079e5 r7c3e6a8  
    11import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
     2import { VoteType } from 'src/app/shared-app/models';
    23import { ButtonType } from '../button/button.models';
    3 
    4 export enum VoteType {
    5   Upvote,
    6   Downvote
    7 }
    84
    95@Component({
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.html

    rad079e5 r7c3e6a8  
    1414      <mat-card-content>
    1515        <div fxLayout="row wrap" fxLayoutAlign="space-around none">
    16           <app-vote [voteCount]="answer.votesCount" [correct]="answer.correctAnswer" fxFlex="6%"></app-vote>
     16          <app-vote
     17            [voteCount]="answer.votesCount"
     18            [correct]="answer.correctAnswer"
     19            (voteClicked)="answerVoted($event, answer.uid, question.uid)"
     20            fxFlex="6%"
     21          ></app-vote>
    1722          <div fxFlex="92%">
    1823            <app-text-editor
  • src/Clients/Angular/finki-chattery/src/app/shared-app/components/question/question-preview/question-preview.component.ts

    rad079e5 r7c3e6a8  
     1import { HttpErrorResponse } from '@angular/common/http';
    12import { Component, OnInit } from '@angular/core';
     3import { NotificationService } from 'src/app/core/services/notification.service';
    24import { QuestionFacadeService } from 'src/app/core/state/question-facade.service';
    35
    4 import { QuestionStateViewModel } from 'src/app/shared-app/models';
     6import { QuestionStateViewModel, VoteType } from 'src/app/shared-app/models';
    57import { ButtonType } from '../../generic/button/button.models';
    68
     
    1416  working = true;
    1517  ButtonType = ButtonType;
    16   constructor(private questionFacade: QuestionFacadeService) {}
     18  constructor(private questionFacade: QuestionFacadeService, private notification: NotificationService) {}
    1719
    1820  ngOnInit(): void {
     
    2123      this.working = false;
    2224    });
     25
     26    this.questionFacade.effectWorking$.subscribe((effect) => {
     27      if (effect instanceof HttpErrorResponse) {
     28        this.notification.handleErrorsNotification(effect.error);
     29      }
     30    });
     31  }
     32
     33  answerVoted(voteType: VoteType, answerUid: string, questionUid: string): void {
     34    this.questionFacade.voteAnswer(answerUid, questionUid, voteType);
    2335  }
    2436}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-enums.models.ts

    rad079e5 r7c3e6a8  
    33  Popular
    44}
     5
     6export enum VoteType {
     7  Upvote,
     8  Downvote
     9}
  • src/Clients/Angular/finki-chattery/src/app/shared-app/models/question-state-view-models.models.ts

    rad079e5 r7c3e6a8  
     1import { VoteType } from '.';
     2
    13export class QuestionStateViewModel {
    24  constructor(
     
    7375  constructor(public text: string) {}
    7476}
     77
     78export class VoteAnswerViewModel {
     79  constructor(public answerUid: string, public voteUid: string, public voteType: VoteType) {}
     80}
  • src/Clients/Angular/finki-chattery/src/assets/translations/en.json

    rad079e5 r7c3e6a8  
    5353  "ask-question-stepper-ask": "Ask question",
    5454  "ask-question-ask-button-back": "Edit question",
    55   "ask-question-ask-button": "Ask question"
     55  "ask-question-ask-button": "Ask question",
     56  "AnswerAlreadyUpvoted": "You have already upvoted this answer",
     57  "AnswerAlreadyDownvoted": "You have already downvoted this answer",
     58  "StudentHasBadReputation": "You have bad reputation and can not vote"
    5659}
Note: See TracChangeset for help on using the changeset viewer.