diff --git a/src/adapters/Survey/index.ts b/src/adapters/Survey/index.ts index 4a0636d..057b933 100644 --- a/src/adapters/Survey/index.ts +++ b/src/adapters/Survey/index.ts @@ -1,8 +1,11 @@ import { getAuth, postAuth } from 'adapters/BaseAuth'; +import { JSONObject } from 'helpers/json'; import { SurveySubmitRequest } from 'types/request/surveySubmitRequest'; export const getSurveys = () => getAuth('surveys?page[number]=1&page[size]=10'); export const getSurvey = (surveyId: string) => getAuth(`surveys/${surveyId}`); -export const submitSurvey = (surveySubmitRequest: SurveySubmitRequest) => postAuth('responses', surveySubmitRequest); +export const submitSurvey = (surveySubmitRequest: SurveySubmitRequest) => { + return postAuth('responses', surveySubmitRequest as unknown as JSONObject); +}; diff --git a/src/components/Answer/index.tsx b/src/components/Answer/index.tsx index a9ed91c..6793a1a 100644 --- a/src/components/Answer/index.tsx +++ b/src/components/Answer/index.tsx @@ -8,12 +8,14 @@ import Nps from 'components/Nps'; import Rating from 'components/Rating'; import TextArea from 'components/TextArea'; import { DisplayType, Question, getDisplayTypeEnum } from 'types/question'; +import { AnswerRequest } from 'types/request/surveySubmitRequest'; interface AnswerProps { question: Question; + onAnswerChanged?: (answers: AnswerRequest[]) => void; 'data-test-id'?: string; } -const Answer = ({ question, ...rest }: AnswerProps): JSX.Element => { +const Answer = ({ question, onAnswerChanged, ...rest }: AnswerProps): JSX.Element => { const displayTypeEnum = getDisplayTypeEnum(question); switch (displayTypeEnum) { @@ -27,6 +29,7 @@ const Answer = ({ question, ...rest }: AnswerProps): JSX.Element => { items={question.answers} displayType={displayTypeEnum} data-test-id={rest['data-test-id']} + onValueChanged={(answer) => onAnswerChanged?.([{ id: answer.id }])} /> ); case DisplayType.Choice: @@ -36,18 +39,64 @@ const Answer = ({ question, ...rest }: AnswerProps): JSX.Element => { items={question.answers} isPickOne={question?.pick === 'one'} data-test-id={rest['data-test-id']} + onValuesChanged={(answers) => { + const answerRequests: AnswerRequest[] = answers.map((answer) => ({ + id: answer.id, + })); + return onAnswerChanged?.(answerRequests); + }} /> ); case DisplayType.Nps: - return ; + return ( + { + const answerRequests: AnswerRequest[] = answers.map((answer) => ({ + id: answer.id, + })); + return onAnswerChanged?.(answerRequests); + }} + /> + ); case DisplayType.Textarea: - return ; + return ( + onAnswerChanged?.([answerRequest])} + /> + ); case DisplayType.Textfield: - return ; + return ( + onAnswerChanged?.(answerRequests)} + /> + ); case DisplayType.Dropdown: - return ; + return ( + onAnswerChanged?.([{ id: answer.id }])} + /> + ); case DisplayType.Slider: - return ; + return ( + onAnswerChanged?.([{ id: answer.id }])} + /> + ); case DisplayType.Unknown: case DisplayType.Intro: case DisplayType.Outro: diff --git a/src/components/MultiInputs/index.tsx b/src/components/MultiInputs/index.tsx index 2f0a5f9..0f13736 100644 --- a/src/components/MultiInputs/index.tsx +++ b/src/components/MultiInputs/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import TextInput from 'components/TextInput'; import { Answer } from 'types/answer'; @@ -14,26 +14,17 @@ const MultiInputs = ({ questionId, items, onValuesChanged, ...rest }: MultiInput const [selectedValues, setSelectedValues] = useState([]); const handleValuesChanged = (answer: Answer, content: string) => { - const newSelectedValues = selectedValues; - const itemIndex = newSelectedValues.findIndex((value) => value.id === answer.id); - if (itemIndex !== -1) { - newSelectedValues[itemIndex].answer = content; - } + const newSelectedValues = [ + ...selectedValues.filter((value) => value.id !== answer.id), + { + id: answer.id, + answer: content, + }, + ]; setSelectedValues(newSelectedValues); onValuesChanged?.(newSelectedValues); }; - useEffect(() => { - const newSelectedValues: AnswerRequest[] = []; - for (let i = 0; i < items.length; i++) { - newSelectedValues.push({ - id: items[i].id, - answer: '', - }); - } - setSelectedValues(newSelectedValues); - }, [questionId, items]); - return ( {items.map((item) => ( diff --git a/src/components/TextArea/index.tsx b/src/components/TextArea/index.tsx index 69a5e5c..45b65a2 100644 --- a/src/components/TextArea/index.tsx +++ b/src/components/TextArea/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Answer } from 'types/answer'; import { AnswerRequest } from 'types/request/surveySubmitRequest'; @@ -11,24 +11,14 @@ interface TextAreaProps { } const TextArea = ({ questionId, items, onValueChange, ...rest }: TextAreaProps): JSX.Element => { - const [selectedValue, setSelectedValue] = useState({ - id: items[0].id, - answer: '', - }); const handleOnChange = (event: React.ChangeEvent) => { event.preventDefault(); - const answerRequest: AnswerRequest = selectedValue; - answerRequest.answer = event.target.value; - setSelectedValue(answerRequest); - onValueChange?.(answerRequest); - }; - - useEffect(() => { - setSelectedValue({ + onValueChange?.({ id: items[0].id, - answer: '', + answer: event.target.value, }); - }, [questionId, items]); + }; + return ( { @@ -20,6 +22,11 @@ export const questionPath = (): string => { return questionPaths[questionPaths.length - 1]; }; +export const questionCompletePath = (): string => { + const questionCompletePaths = paths.questionComplete.split('/'); + return questionCompletePaths[questionCompletePaths.length - 1]; +}; + const routes: RouteObject[] = [ { element: , @@ -36,6 +43,10 @@ const routes: RouteObject[] = [ path: paths.question, element: , }, + { + path: paths.questionComplete, + element: , + }, ], }, { diff --git a/src/screens/Question/Complete/index.tsx b/src/screens/Question/Complete/index.tsx new file mode 100644 index 0000000..f429b8d --- /dev/null +++ b/src/screens/Question/Complete/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const QuestionCompleteScreen = (): JSX.Element => { + return Complete; +}; + +export default QuestionCompleteScreen; diff --git a/src/screens/Question/index.test.tsx b/src/screens/Question/index.test.tsx index a25e391..c38bf6d 100644 --- a/src/screens/Question/index.test.tsx +++ b/src/screens/Question/index.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { act, render, screen } from '@testing-library/react'; import { useAppDispatch, useAppSelector } from 'hooks'; +import { paths } from 'routes'; import { SurveyState } from 'store/reducers/Survey'; import TestWrapper from 'tests/TestWrapper'; @@ -71,6 +72,8 @@ describe('QuestionScreen', () => { }, isLoading: true, isError: false, + questionRequests: [], + isSubmitSuccess: false, }, }; @@ -120,7 +123,7 @@ describe('QuestionScreen', () => { }); describe('given the close button is clicked', () => { - it('navigates back to the previous screen', () => { + it('navigates back to the Home screen', () => { render(); const closeButton = screen.getByTestId(questionScreenTestIds.closeButton); @@ -129,7 +132,35 @@ describe('QuestionScreen', () => { closeButton.click(); }); - expect(mockUseNavigate).toHaveBeenCalledWith(-1); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'survey/resetQuestions' }); + expect(mockUseNavigate).toHaveBeenCalledWith(paths.root, { replace: true }); + }); + }); + + describe('given the submit button is clicked', () => { + it('submits the survey', () => { + render(); + + const nextButton = screen.getByTestId(questionScreenTestIds.nextButton); + + act(() => { + nextButton.click(); + }); + + act(() => { + nextButton.click(); + }); + + const submitButton = screen.getByTestId(questionScreenTestIds.submitButton); + + act(() => { + submitButton.click(); + }); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'survey/fillAnswers', + payload: { id: 'question 2', answers: [{ id: 'answer 1' }] }, + }); }); }); }); diff --git a/src/screens/Question/index.tsx b/src/screens/Question/index.tsx index ec43da0..7c41cf3 100644 --- a/src/screens/Question/index.tsx +++ b/src/screens/Question/index.tsx @@ -1,12 +1,17 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { ToastContainer, toast } from 'react-toastify'; import { ReactComponent as ArrowRight } from 'assets/images/icons/arrow-right.svg'; import { ReactComponent as CloseButton } from 'assets/images/icons/close-btn-white.svg'; import Answer from 'components/Answer'; import BackgroundImage from 'components/BackgroundImage'; import ElevatedButton from 'components/ElevatedButton'; -import { useAppSelector } from 'hooks'; +import LoadingDialog from 'components/LoadingDialog'; +import { useAppDispatch, useAppSelector } from 'hooks'; +import { paths, questionCompletePath } from 'routes'; +import { submitSurveyAsyncThunk, surveyAction } from 'store/reducers/Survey'; +import { AnswerRequest } from 'types/request/surveySubmitRequest'; export const questionScreenTestIds = { index: 'question__index', @@ -14,15 +19,26 @@ export const questionScreenTestIds = { answer: 'question__answer', closeButton: 'question__close-button', nextButton: 'question__next-button', + submitButton: 'question__submit-button', }; const QuestionScreen = (): JSX.Element => { - const { survey } = useAppSelector((state) => state.survey); + const { survey, questionRequests, isLoading, isError, isSubmitSuccess } = useAppSelector((state) => state.survey); + const dispatch = useAppDispatch(); const navigate = useNavigate(); const [currentQuestion, setCurrentQuestion] = useState(survey?.questions?.at(0)); const [questionIndex, setQuestionIndex] = useState(0); + const handleAnswerChanged = (answers: AnswerRequest[]) => { + dispatch( + surveyAction.fillAnswers({ + id: currentQuestion?.id ?? '', + answers: answers, + }) + ); + }; + const onNextClick = () => { const nextQuestionIndex = questionIndex + 1; if (nextQuestionIndex >= (survey?.questions?.length ?? 0)) { @@ -33,18 +49,37 @@ const QuestionScreen = (): JSX.Element => { }; const onSubmitClick = () => { - // TODO submit survey + dispatch( + submitSurveyAsyncThunk({ + surveyId: survey?.id ?? '', + questions: questionRequests, + }) + ); }; - function goBack() { - navigate(-1); + function handleOnClose() { + dispatch(surveyAction.resetQuestions()); + navigate(paths.root, { replace: true }); } + useEffect(() => { + if (isSubmitSuccess) { + navigate(questionCompletePath()); + } + }, [isSubmitSuccess, navigate]); + + useEffect(() => { + if (isError) { + toast.error('There is something wrong. Please try again later!', { position: 'top-center' }); + } + }, [isError]); + return ( + - + @@ -62,13 +97,17 @@ const QuestionScreen = (): JSX.Element => { {currentQuestion && ( - + handleAnswerChanged(answers)} + /> )} {questionIndex === (survey?.questions?.length ?? 0) - 1 ? ( - + Submit @@ -84,6 +123,7 @@ const QuestionScreen = (): JSX.Element => { )} + {isLoading && } ); }; diff --git a/src/screens/Survey/index.test.tsx b/src/screens/Survey/index.test.tsx index 0637a3f..c77178a 100644 --- a/src/screens/Survey/index.test.tsx +++ b/src/screens/Survey/index.test.tsx @@ -32,6 +32,8 @@ describe('SurveyScreen', () => { }, isLoading: true, isError: false, + questionRequests: [], + isSubmitSuccess: false, }, }; diff --git a/src/store/reducers/Survey/action.ts b/src/store/reducers/Survey/action.ts index 3390f58..794634e 100644 --- a/src/store/reducers/Survey/action.ts +++ b/src/store/reducers/Survey/action.ts @@ -1,10 +1,25 @@ -import { AsyncThunkPayloadCreator } from '@reduxjs/toolkit'; +import { AsyncThunkPayloadCreator, PayloadAction } from '@reduxjs/toolkit'; -import { getSurvey } from 'adapters/Survey'; +import { getSurvey, submitSurvey } from 'adapters/Survey'; import { DeserializableResponse, deserialize } from 'helpers/deserializer'; import { JSONObject } from 'helpers/json'; +import { QuestionRequest, SurveySubmitRequest } from 'types/request/surveySubmitRequest'; import { Survey } from 'types/survey'; +import { SurveyState } from '.'; + +export const surveyReducers = { + fillAnswers: (state: SurveyState, action: PayloadAction) => { + const newQuestionRequests = state.questionRequests.filter((question: QuestionRequest) => question.id !== action.payload.id); + state.questionRequests = [...newQuestionRequests, action.payload]; + return state; + }, + resetQuestions: (state: SurveyState) => { + state.questionRequests = []; + return state; + }, +}; + export const getSurveyThunkCreator: AsyncThunkPayloadCreator = async (surveyId: string) => { return getSurvey(surveyId).then((response: DeserializableResponse) => { const survey = deserialize(response.data, response.included); @@ -12,3 +27,9 @@ export const getSurveyThunkCreator: AsyncThunkPayloadCreator = async ( + surveySubmitRequest +) => { + await submitSurvey(surveySubmitRequest); +}; diff --git a/src/store/reducers/Survey/index.test.ts b/src/store/reducers/Survey/index.test.ts index 3ff4243..e47ed5c 100644 --- a/src/store/reducers/Survey/index.test.ts +++ b/src/store/reducers/Survey/index.test.ts @@ -54,6 +54,8 @@ describe('survey slice', () => { const mockEmptyState = { isLoading: true, isError: false, + questionRequests: [], + isSubmitSuccess: false, }; describe('getSurveyAsyncThunk.pending', () => { diff --git a/src/store/reducers/Survey/index.ts b/src/store/reducers/Survey/index.ts index 09fd1ab..606cdb5 100644 --- a/src/store/reducers/Survey/index.ts +++ b/src/store/reducers/Survey/index.ts @@ -1,28 +1,33 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; -import { SurveySubmitRequest } from 'types/request/surveySubmitRequest'; +import { QuestionRequest } from 'types/request/surveySubmitRequest'; import { Survey } from 'types/survey'; -import { getSurveyThunkCreator } from './action'; +import { getSurveyThunkCreator, submitSurveyThunkCreator, surveyReducers } from './action'; export interface SurveyState { survey?: Survey; isLoading: boolean; isError: boolean; - surveySubmitRequest?: SurveySubmitRequest; + questionRequests: QuestionRequest[]; + isSubmitSuccess: boolean; } export const initialState: SurveyState = { - isLoading: true, + isLoading: false, isError: false, + questionRequests: [], + isSubmitSuccess: false, }; export const getSurveyAsyncThunk = createAsyncThunk('survey/getSurvey', getSurveyThunkCreator); +export const submitSurveyAsyncThunk = createAsyncThunk('survey/submitSurvey', submitSurveyThunkCreator); + export const surveySlice = createSlice({ name: 'survey', initialState, - reducers: {}, + reducers: surveyReducers, extraReducers: (builder) => { builder.addCase(getSurveyAsyncThunk.pending, (state) => { state.isLoading = true; @@ -37,6 +42,20 @@ export const surveySlice = createSlice({ state.isLoading = false; state.isError = true; }); + builder.addCase(submitSurveyAsyncThunk.pending, (state) => { + state.isLoading = true; + state.isError = false; + }); + builder.addCase(submitSurveyAsyncThunk.fulfilled, (state) => { + state.isLoading = false; + state.isSubmitSuccess = true; + state.questionRequests = []; + }); + builder.addCase(submitSurveyAsyncThunk.rejected, (state) => { + state.isLoading = false; + state.isSubmitSuccess = false; + state.isError = true; + }); }, }); diff --git a/src/types/request/surveySubmitRequest.ts b/src/types/request/surveySubmitRequest.ts index e62c003..aa4ad56 100644 --- a/src/types/request/surveySubmitRequest.ts +++ b/src/types/request/surveySubmitRequest.ts @@ -1,16 +1,14 @@ -import { JSONObject } from 'helpers/json'; - -export interface SurveySubmitRequest extends JSONObject { +export interface SurveySubmitRequest { surveyId: string; questions: QuestionRequest[]; } -interface QuestionRequest extends JSONObject { +export interface QuestionRequest { id: string; answers: AnswerRequest[]; } -export interface AnswerRequest extends JSONObject { +export interface AnswerRequest { id: string; answer?: string; }