aboutsummaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
authorMax Nanis2025-06-06 22:40:41 +0700
committerMax Nanis2025-06-06 22:40:41 +0700
commit257bc2f85b71a8564e95a8e6ba39ab0b00e022df (patch)
treec44ced82b5f5f99e7e7bb9cffcd444a41b8a648c /src/pages
parent696dee6a6a9506fcf771d0ec4911dcc82a279fda (diff)
downloadpanel-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.tsx121
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>
+ </>
)
}