From 4bf1b3f5bc9564070dba1481e380179431ddf62b Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Wed, 4 Jun 2025 05:35:57 +0700 Subject: Saving answers to server. Updating views based on reducer states (pagination disable after question is answered). --- src/models/answerSlice.ts | 118 ++++++++++++--------------------- src/pages/Questions.tsx | 162 +++++++++++++++++++++++++++++++++------------- 2 files changed, 158 insertions(+), 122 deletions(-) diff --git a/src/models/answerSlice.ts b/src/models/answerSlice.ts index f271288..e0f9931 100644 --- a/src/models/answerSlice.ts +++ b/src/models/answerSlice.ts @@ -4,7 +4,7 @@ import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit' import {RootState} from '@/store'; // your root state type import {PatternValidation, UpkQuestion} from "@/api"; -interface Answer { +export interface Answer { values: string[]; error_msg: string; @@ -23,8 +23,17 @@ const answerSlice = createSlice({ name: 'answers', initialState, reducers: { - addAnswer(state, action: PayloadAction<{ question: UpkQuestion, val: string }>) { + /* If the question is MC, validate: + - validate selector SA vs MA (1 selected vs >1 selected) + - the answers match actual codes in the choices + - validate configuration.max_select + - validate choices.exclusive + + If the question is TE, validate that: + - configuration.max_length + - validation.patterns + */ let question: UpkQuestion = action.payload.question; let val: string = action.payload.val.trim(); let answer: Answer = state[question.question_id] ?? { @@ -34,17 +43,7 @@ const answerSlice = createSlice({ processing: false } as Answer; - /* - If the question is MC, validate: - - validate selector SA vs MA (1 selected vs >1 selected) - - the answers match actual codes in the choices - - validate configuration.max_select - - validate choices.exclusive - - If the question is TE, validate that: - - configuration.max_length - - validation.patterns - */ + answer.error_msg = "" // Reset any error messages switch (question.question_type) { case "TE": @@ -78,26 +77,22 @@ const answerSlice = createSlice({ } }) - answer.error_msg = "" break; case "MC": - switch (question.selector) { - case "SA": // Single Answer + if (answer.values.includes(val)) { + // The item has already been selected + answer.values = answer.values.filter(value => value !== val); + + } else { + // It's a new selection + if (question.selector == "SA") { answer.values = [val] - break - case "MA": /// Multi Answer - if (answer.values.includes(val)) { - // The item has already been selected - answer.values = answer.values.filter(value => value !== val); - } else { - // It's a new selection - answer.values.push(val); - } - break + } else if (question.selector == "MA") { + answer.values.push(val); + } } - if (answer.values.length == 0) { answer.error_msg = "MC question with no selected answers" } @@ -145,62 +140,29 @@ const answerSlice = createSlice({ state[question.question_id] = answer }, - // removeAnswer(val: string): null { - // switch (this.getType()) { - // // You can only remove a value from a MultiChoice - // case "MC": - // // TODO: implement this - // // let current_values: string[] = this._answer?.values - // // current_values.push(val) - // // this._answer = new ProfilingAnswer(this.questionId, current_values); - // break - // default: - // throw new Error("Incorrect Question Type provided"); - // } - // this.validate() - // } - - // save() { - // let question: ProfilingQuestion = this; - // // @ts-ignore - // let answer: ProfilingAnswer = question._answer; - // - // if (this._complete || this._processing) { - // return - // } - // this._processing = true - // - // let res = JSON.stringify({ - // "answers": [{ - // "question_id": answer.get('question_id'), - // "answer": map(answer.get("values"), "value") - // }] - // }); - // - // $.ajax({ - // url: ["https://fsb.generalresearch.com", questions.BPID, "profiling-questions", ""].join("/") + "?" + stringify({"bpuid": questions.BPUID}), - // xhrFields: {withCredentials: false}, - // processData: false, - // type: "POST", - // contentType: "application/json; charset=utf-8", - // data: res, - // success: function (data) { - // channel.trigger("ProfilingQuestions:start"); - // }, - // error: function (data) { - // channel.trigger("ProfilingQuestions:start"); - // Sentry.captureMessage("Profiling Question submission failed."); - // } - // }); - // } - - + saveAnswer(state, action: PayloadAction<{ question: UpkQuestion }>) { + let question: UpkQuestion = action.payload.question; + let answer: Answer = state[question.question_id] + + state[question.question_id] = { + 'values': answer.values, + 'error_msg': "", + 'processing': false, + 'complete': false + } as Answer + } } }) -export const {addAnswer, setAnswer, setQuestions, questionAdded, questionUpdated} = answerSlice.actions; +export const {addAnswer, saveAnswer} = answerSlice.actions; export default answerSlice.reducer +export const answerForQuestion = (state: RootState, question: UpkQuestion) => state.answers[question.question_id] ?? { + values: [], + error_msg: "", + complete: false, + processing: false +} as Answer; export const makeSelectChoicesByQuestion = (question: UpkQuestion) => createSelector( diff --git a/src/pages/Questions.tsx b/src/pages/Questions.tsx index a6f0d7b..e9abe31 100644 --- a/src/pages/Questions.tsx +++ b/src/pages/Questions.tsx @@ -1,8 +1,14 @@ import React, {useMemo, useState} from 'react' -import {UpkQuestion, UpkQuestionChoice} from "@/api"; -import {Card, CardContent, CardTitle, CardFooter, CardHeader} from "@/components/ui/card.tsx"; +import { + BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost, + ProfilingQuestionsApiFactory, + UpkQuestion, + UpkQuestionChoice, + UserQuestionAnswerIn +} from "@/api"; +import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card.tsx"; import {useAppDispatch, useAppSelector} from "@/hooks.ts"; -import {addAnswer, makeSelectChoicesByQuestion} from "@/models/answerSlice.ts"; +import answerSlice, {addAnswer, Answer,answerForQuestion, makeSelectChoicesByQuestion, saveAnswer} from "@/models/answerSlice.ts"; import {useSelector} from "react-redux"; import {Button} from "@/components/ui/button" import { @@ -12,41 +18,49 @@ import { PaginationLink, PaginationNext, } from "@/components/ui/pagination" -import { Input } from "@/components/ui/input" +import {Input} from "@/components/ui/input" import {Badge} from "@/components/ui/badge" const TextEntry: React.FC<{ question: UpkQuestion }> = ({question}) => { const dispatch = useAppDispatch() const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]); - const answer = useSelector(selectAnswer); + const answer: Answer = useSelector(selectAnswer); + const error: Boolean = answer.error_msg.length > 0 const handleInputChange = (event: React.ChangeEvent) => { dispatch(addAnswer({question: question, val: event.target.value})) }; - // console.log("TextEntry.answer", answer) - return ( - - // {answer.error_msg} + <> + + + { + error &&

{answer.error_msg}

+ } + + ) } const MultiChoiceItem: React.FC<{ question: UpkQuestion, choice: UpkQuestionChoice }> = ({question, choice}) => { const dispatch = useAppDispatch() const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]); - const answer = useSelector(selectAnswer); + const answer: Answer = useSelector(selectAnswer); const selected: Boolean = answer.values.includes(choice.choice_id) return (
  • ) } +// type Props = { +// onSetQuestionID: (name: string) => void +// } + +const PaginationIcon: React.FC<{ + question: UpkQuestion, activeQuestionID: string, idx: number, onSetQuestionID: () => void +}> = ({question, activeQuestionID, idx, onSetQuestionID}) => { + + const answers = useAppSelector(state => state.answers) + const answer = answers[question.question_id] + + return ( + + answer?.complete ? e.preventDefault() : onSetQuestionID(question.question_id)} + className={answer?.complete ? "pointer-events-none opacity-50 cursor-not-allowed" : ""}> + {idx + 1} + + + ) +} const QuestionsPage = () => { const questions = useAppSelector(state => state.questions) - const [activeQuestionID, setQuestionID] = useState(() => questions[0].question_id); + const [activeQuestionID, setQuestionID] = useState(() => questions[0].question_id); const question = questions.find(q => q.question_id === activeQuestionID); - console.log("activeQuestionID:", activeQuestionID, question) return (
    @@ -129,18 +214,8 @@ const QuestionsPage = () => { { questions.slice(0, 5).map((q, i) => { - return ( - - setQuestionID(q.question_id)} - isActive={q.question_id === activeQuestionID} - > - {i + 1} - - - ) + return }) } @@ -152,7 +227,6 @@ const QuestionsPage = () => { -
    ) -- cgit v1.2.3