diff options
| author | Max Nanis | 2025-06-06 22:40:41 +0700 |
|---|---|---|
| committer | Max Nanis | 2025-06-06 22:40:41 +0700 |
| commit | 257bc2f85b71a8564e95a8e6ba39ab0b00e022df (patch) | |
| tree | c44ced82b5f5f99e7e7bb9cffcd444a41b8a648c /src/pages | |
| parent | 696dee6a6a9506fcf771d0ec4911dcc82a279fda (diff) | |
| download | panel-ui-257bc2f85b71a8564e95a8e6ba39ab0b00e022df.tar.gz panel-ui-257bc2f85b71a8564e95a8e6ba39ab0b00e022df.zip | |
Question.active state (clear naming on getInitialQuestion and getNextQuestion). Explicit use of return null as an option for answerSlice. Saving motion. Questions rolling window. Question count badge to sidebar.
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/Questions.tsx | 121 |
1 files changed, 84 insertions, 37 deletions
diff --git a/src/pages/Questions.tsx b/src/pages/Questions.tsx index 1056144..3cd6f71 100644 --- a/src/pages/Questions.tsx +++ b/src/pages/Questions.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react' +import React from 'react' import { BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost, ProfilingQuestionsApiFactory, @@ -7,7 +7,7 @@ import { } from "@/api"; import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card.tsx"; import {useAppDispatch, useAppSelector} from "@/hooks.ts"; -import {addAnswer, Answer, makeSelectChoicesByQuestion, saveAnswer, submitAnswer} from "@/models/answerSlice.ts"; +import {addAnswer, Answer, saveAnswer, selectAnswerForQuestion, submitAnswer} from "@/models/answerSlice.ts"; import {useSelector} from "react-redux"; import {Button} from "@/components/ui/button" import { @@ -22,18 +22,21 @@ import {Badge} from "@/components/ui/badge" import clsx from "clsx" import { ProfileQuestion, + selectFirstAvailableQuestion, selectNextAvailableQuestion, selectQuestions, setNextQuestion, setQuestionActive } from "@/models/questionSlice.ts"; import {assert} from "@/lib/utils.ts"; +import {motion} from "framer-motion" const TextEntry: React.FC<{ question: ProfileQuestion }> = ({question}) => { const dispatch = useAppDispatch() - const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]); - const answer: Answer = useSelector(selectAnswer); - const error: Boolean = answer.error_msg.length > 0 + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const selectAnswer = useSelector(selectAnswerForQuestion(question)); + const answer: Answer | undefined = useSelector(selectAnswerForQuestion(question)); + const error: Boolean = answer?.error_msg.length > 0 const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { dispatch(addAnswer({question: question, val: event.target.value})) @@ -44,14 +47,14 @@ const TextEntry: React.FC<{ question: ProfileQuestion }> = ({question}) => { <Input type="text" id="text-entry-input" aria-describedby="" - defaultValue={answer.values.length ? answer.values[0] : ""} + defaultValue={answer?.values.length ? answer?.values[0] : ""} onKeyUp={handleInputChange} - title={error ? answer.error_msg : ""} + title={error ? answer?.error_msg : ""} className={error ? "border-red-500 focus-visible:ring-red-500" : ""} /> { - error && <p className="text-sm text-red-500 mt-1">{answer.error_msg}</p> + error && <p className="text-sm text-red-500 mt-1">{answer?.error_msg}</p> } </> @@ -60,9 +63,11 @@ const TextEntry: React.FC<{ question: ProfileQuestion }> = ({question}) => { const MultiChoiceItem: React.FC<{ question: ProfileQuestion, choice: UpkQuestionChoice }> = ({question, choice}) => { const dispatch = useAppDispatch() - const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]); - const answer: Answer = useSelector(selectAnswer); - const selected: Boolean = answer.values.includes(choice.choice_id) + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const answer: Answer = useSelector(selectAnswer); + const answer: Answer | undefined = useSelector(selectAnswerForQuestion(question)); + + const selected: Boolean = (answer?.values || []).includes(choice.choice_id) return ( <li key={choice.choice_id} style={{marginBottom: '0.5rem'}}> @@ -78,14 +83,16 @@ const MultiChoiceItem: React.FC<{ question: ProfileQuestion, choice: UpkQuestion } const MultipleChoice: React.FC<{ question: ProfileQuestion }> = ({question}) => { - const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]); - const answer: Answer = useSelector(selectAnswer); - const error: Boolean = answer.error_msg.length > 0 + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const answer: Answer = useSelector(selectAnswer); + + const answer: Answer | undefined = useSelector(selectAnswerForQuestion(question)); + const error: Boolean = answer?.error_msg.length > 0 return ( <> { - error && <p className="text-sm text-red-500 mt-1">{answer.error_msg}</p> + error && <p className="text-sm text-red-500 mt-1">{answer?.error_msg}</p> } <ol style={{listStyle: 'none', padding: 0, margin: 0}}> @@ -109,13 +116,14 @@ const ProfileQuestionFull: React.FC<{ const dispatch = useAppDispatch() - const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]); - const answer: Answer = useSelector(selectAnswer); + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const answer: Answer = useSelector(selectAnswer); + const answer: Answer | undefined = useSelector(selectAnswerForQuestion(question)); const app = useAppSelector(state => state.app) - const provided_answer = answer.values.length > 0 - const error: Boolean = answer.error_msg.length > 0 - const can_submit = provided_answer && !error && !answer.complete + const provided_answer = answer?.values.length > 0 + const error: Boolean = answer?.error_msg.length > 0 + const can_submit = provided_answer && !error && !answer?.complete const renderContent = () => { switch (question.question_type) { @@ -129,9 +137,9 @@ const ProfileQuestionFull: React.FC<{ const submitAnswerEvt = () => { dispatch(submitAnswer({question: question})) - assert(!answer.complete, "Can't submit completed Answer") - assert(!answer.processing, "Can't submit processing Answer") - assert(answer.error_msg.length == 0, "Can't submit Answer with error message") + assert(!answer?.complete, "Can't submit completed Answer") + assert(!answer?.processing, "Can't submit processing Answer") + assert(answer?.error_msg.length == 0, "Can't submit Answer with error message") let body: BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost = { 'answers': [{ @@ -153,7 +161,17 @@ const ProfileQuestionFull: React.FC<{ } return ( - <Card className="@container/card relative"> + <Card className="@container/card relative overflow-hidden"> + {answer && answer.processing && ( + <motion.div + className="absolute top-0 left-0 h-0.5 bg-gray-300" + initial={{width: "0%"}} + animate={{width: "100%"}} + transition={{duration: 1, ease: "easeInOut"}} + // onAnimationComplete={() => setLoading(false)} + /> + )} + <Badge className="absolute top-2 right-2 h-5 min-w-5 rounded-full px-1 font-mono tabular-nums cursor-pointer" variant="outline" @@ -219,35 +237,64 @@ const PaginationIcon: React.FC<{ ) } + const QuestionsPage = () => { const dispatch = useAppDispatch() const questions = useSelector(selectQuestions) - const question = useSelector(selectNextAvailableQuestion) - dispatch(setQuestionActive(question)) + const question = useSelector(selectFirstAvailableQuestion) + dispatch(setQuestionActive(question as ProfileQuestion)) + + // This is saved now, so that if they click next it's ready. It + // cannot be done within the click handler. + const nextQuestion = useSelector(selectNextAvailableQuestion) const clickNext = () => { // TODO: if nextQuestion was already submitted, skip it! + if (nextQuestion) { + // TS is not smart enough to know that the if statement above + // prevents this from ever being null + dispatch(setQuestionActive(nextQuestion as ProfileQuestion)) + } else { + // What do we do now... no more questions left to do. + } + } - const index = questions.findIndex(q => q.question_id === question.question_id) - const nextQuestion = index !== -1 ? questions[index + 1] ?? null : null - dispatch(setQuestionActive(nextQuestion)) + // All the variables needed for a sliding window + const q_idx = questions.findIndex(q => q.question_id === question.question_id) + const questionsWindow = ( + items: ProfileQuestion[], currentIndex: number, windowSize: number = 7 + ): ProfileQuestion[] => { + const half: number = Math.floor(windowSize / 2) + const total: number = items.length + let start: number = currentIndex - half + let end: number = currentIndex + half + 1 + + if (start < 0) { + end += Math.abs(start) + start = 0 + } + + // Adjust if window goes past the end + if (end > total) { + const overflow: number = end - total + start = Math.max(0, start - overflow) + end = total + } + + return items.slice(start, end) } return ( - <div> - <p> - A total of {questions.length} questions are available. - </p> - + <> <Pagination className="mt-4 mb-4"> <PaginationContent> { - questions.slice(0, 5).map((q, i) => { + questionsWindow(questions, q_idx).map(q => { return <PaginationIcon key={q.question_id} question={q} - idx={i} + idx={questions.findIndex(qq => qq.question_id === q.question_id)} /> }) } @@ -265,7 +312,7 @@ const QuestionsPage = () => { key={question.question_id} question={question} className="mt-4 mb-4"/> - </div> + </> ) } |
