From 62456f41288eb0f03c5092d36610d566f275c8b5 Mon Sep 17 00:00:00 2001
From: Max Nanis
Date: Tue, 3 Jun 2025 20:06:44 +0700
Subject: selectQuestionById, useMemo selector (MultiChoice.choices makes a lot
of lookup). Setting up the Questions view to do Pagination to tab through
available questions and view one at a time
---
src/Widget.tsx | 6 +-
src/components/app-sidebar.tsx | 84 +---------------------
src/components/nav-main.tsx | 6 +-
src/components/site-header.tsx | 2 +-
src/models/answerSlice.ts | 86 +++++++++++++----------
src/models/bucketSlice.ts | 1 -
src/models/questionSlice.ts | 22 +++---
src/pages/Questions.tsx | 154 ++++++++++++++++++++---------------------
src/store.ts | 10 ++-
9 files changed, 154 insertions(+), 217 deletions(-)
diff --git a/src/Widget.tsx b/src/Widget.tsx
index 99c2a33..c3b1f8f 100644
--- a/src/Widget.tsx
+++ b/src/Widget.tsx
@@ -45,9 +45,9 @@ const Widget = () => {
- {app.currentPage === 'offerwall' && }
- {app.currentPage === 'questions' && }
- {app.currentPage === 'cashouts' && }
+ {app.currentPage === 'offerwall' && }
+ {app.currentPage === 'questions' && }
+ {app.currentPage === 'cashouts' && }
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index d2bc03e..e8cbd4c 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -1,19 +1,9 @@
"use client"
import * as React from "react"
-import {CircleDollarSign, MessageCircle, MoreVerticalIcon, SquareStack, UserCircleIcon} from "lucide-react"
+import {CircleDollarSign, MessageCircle, SquareStack} from "lucide-react"
import {NavMain} from "@/components/nav-main"
-import {Avatar, AvatarFallback, AvatarImage,} from "@/components/ui/avatar"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
import {
Sidebar,
SidebarContent,
@@ -34,15 +24,11 @@ export function AppSidebar({...props}: React.ComponentProps) {
const {isMobile} = useSidebar()
- //
- //
- //
-
return (
-
+
) {
-
-
-
-
-
-
- Support
-
-
-
-
-
-
-
-
-
-
-
-
-
- IW
-
-
- Ironwood User
-
- ironwood@example.com
-
-
-
-
-
-
-
-
-
-
- IW
-
-
- Ironwood User
-
- ironwood@example.com
-
-
-
-
-
-
-
-
- Account
-
-
-
-
-
-
-
)
}
diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx
index 873d236..12a60aa 100644
--- a/src/components/nav-main.tsx
+++ b/src/components/nav-main.tsx
@@ -19,7 +19,7 @@ export function NavMain() {
- dispatch(setPage("offerwall"))}
>
@@ -29,7 +29,7 @@ export function NavMain() {
- dispatch(setPage("questions"))}
>
@@ -38,7 +38,7 @@ export function NavMain() {
-
+
Community
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index 5bc5ca7..0000e5f 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -14,7 +14,7 @@ const SiteHeader = () => {
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
- "activeView"
+ Offerwall
diff --git a/src/models/answerSlice.ts b/src/models/answerSlice.ts
index 368072b..f271288 100644
--- a/src/models/answerSlice.ts
+++ b/src/models/answerSlice.ts
@@ -1,13 +1,8 @@
-import {createSlice, PayloadAction} from '@reduxjs/toolkit'
-// import type {RootState} from '@/store'
+import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit'
// import {Answer} from "@/models/answer.ts";
// import {stringify} from "querystring";
-// import {PatternType} from "@/types.ts";
-// import {UserQuestionAnswerIn} from "@/api"
import {RootState} from '@/store'; // your root state type
import {PatternValidation, UpkQuestion} from "@/api";
-import {Answer} from "@/models/answer.ts"
-
interface Answer {
values: string[];
@@ -51,30 +46,27 @@ const answerSlice = createSlice({
- validation.patterns
*/
- switch (question.question_type) {
- case "MC":
- answer.values.push(val);
- break
- }
-
switch (question.question_type) {
case "TE":
answer.values = [val]
if (answer.values.length > 1) {
answer.error_msg = "Only one answer allowed"
+ break;
}
let answer_text: string = answer.values[0]
if (answer_text.length <= 0) {
answer.error_msg = "Must provide answer"
+ break;
}
const max_length: number = (question.configuration ?? {})["max_length"] ?? 100000
if (answer_text.length > max_length) {
answer.error_msg = "Answer longer than allowed"
+ break;
}
const patterns: PatternValidation[] = (question.validation ?? {})["patterns"] ?? []
@@ -82,13 +74,30 @@ const answerSlice = createSlice({
let re = new RegExp(pv.pattern)
if (answer_text.search(re) == -1) {
answer.error_msg = pv.message
+ return;
}
})
answer.error_msg = ""
- break
+ break;
case "MC":
+ switch (question.selector) {
+ case "SA": // Single Answer
+ 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
+ }
+
+
if (answer.values.length == 0) {
answer.error_msg = "MC question with no selected answers"
}
@@ -100,26 +109,23 @@ const answerSlice = createSlice({
if (answer.values.length > 1) {
answer.error_msg = "Single Answer MC question with >1 selected answers"
}
- break
+ break;
case "MA":
if (answer.values.length > choice_codes.length) {
answer.error_msg = "More options selected than allowed"
}
- break
+ break;
}
- // if (!every(qa.values, (v) => {
- // return includes(choice_codes, v["value"])
- // })) {
- // this.error_msg = "Invalid Options Selected"
- // return false
- // }
- //
- // const max_select: number = (this.configuration ?? {})["max_select"] ?? choice_codes.length
- // if (qa.values.length > max_select) {
- // this.error_msg = "More options selected than allowed"
- // return false
- // }
+
+ if (!answer.values.every(val => choice_codes.includes(val))) {
+ answer.error_msg = "Invalid Options Selected";
+ }
+
+ const max_select: number = (question.configuration ?? {})["max_select"] ?? choice_codes.length
+ if (answer.values.length > max_select) {
+ answer.error_msg = "More options selected than allowed"
+ }
/*
exclusive_choice = next((x for x in question["choices"] if x.get("exclusive")), None)
@@ -130,18 +136,17 @@ const answerSlice = createSlice({
*/
answer.error_msg = ""
- break
+ break;
default:
throw new Error("Incorrect Question Type provided");
}
- state[question.question_id] = question;
+ 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
@@ -149,15 +154,12 @@ const answerSlice = createSlice({
// // 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
@@ -196,9 +198,21 @@ const answerSlice = createSlice({
}
})
-// Export the generated reducer function
export const {addAnswer, setAnswer, setQuestions, questionAdded, questionUpdated} = answerSlice.actions;
export default answerSlice.reducer
-export const selectAnswerCount = (state: RootState) => state.answers.size
-// export const selectAnswerByQuestionId = (state: RootState, questionId: string) => state.answers.find(a => q.questionId === questionId)
\ No newline at end of file
+
+export const makeSelectChoicesByQuestion = (question: UpkQuestion) =>
+ createSelector(
+ (state: RootState) => state.answers,
+ (answers) => {
+ // const question = questions.find(q => q.id === questionId);
+ // return question?.choices ?? [];
+ return answers[question.question_id] ?? {
+ values: [],
+ error_msg: "",
+ complete: false,
+ processing: false
+ } as Answer;
+ }
+ );
\ No newline at end of file
diff --git a/src/models/bucketSlice.ts b/src/models/bucketSlice.ts
index 3a2c0d1..1a15263 100644
--- a/src/models/bucketSlice.ts
+++ b/src/models/bucketSlice.ts
@@ -23,7 +23,6 @@ export default bucketSlice.reducer
export const selectBucketsStatus = (state: RootState) => state.buckets.status
export const selectBucketsError = (state: RootState) => state.buckets.error
-
export const selectAllBuckets = (state: RootState) => state.buckets
export const selectBucketById = (state: RootState, bucketId: string | null) =>
diff --git a/src/models/questionSlice.ts b/src/models/questionSlice.ts
index e3674be..814caf9 100644
--- a/src/models/questionSlice.ts
+++ b/src/models/questionSlice.ts
@@ -1,4 +1,4 @@
-import {createSlice, PayloadAction} from '@reduxjs/toolkit'
+import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit'
import type {RootState} from '@/store'
import {UpkQuestion} from "@/api";
@@ -14,22 +14,18 @@ const questionSlice = createSlice({
questionAdded(state, action: PayloadAction) {
state.push(action.payload);
},
- setAnswer(state, action: PayloadAction<{ questionId: string, val: string }>) {
- const {questionId, val} = action.payload
- console.log(questionId, val)
- const existingQuestion = state.find(q => q.questionId === action.payload.questionId)
- if (existingQuestion) {
- // existingQuestion.addAnswer(action.payload.val)
- // existingQuestion.error_msg = "yess"
- }
- }
}
})
-// Export the generated reducer function
export const {setAnswer, setQuestions, questionAdded, questionUpdated} = questionSlice.actions;
export default questionSlice.reducer
-export const selectAllQuestions = (state: RootState) => state.questions
+// export const selectAllQuestions = (state: RootState) => state.questions
-export const selectQuestionById = (state: RootState, questionId: string) => state.questions.find(q => q.id === questionId)
\ No newline at end of file
+export const selectQuestionById = (questionId: string) =>
+ createSelector(
+ (state: RootState) => state.questions,
+ (questions) => {
+ return questions.find(q => q.question_id === questionId);
+ }
+ );
\ No newline at end of file
diff --git a/src/pages/Questions.tsx b/src/pages/Questions.tsx
index 0a3f43e..9eb7cba 100644
--- a/src/pages/Questions.tsx
+++ b/src/pages/Questions.tsx
@@ -1,23 +1,29 @@
-import React from 'react'
-import {UpkQuestion} from "@/api";
-import {UpkQuestionChoice} from "@/api/models/upk-question-choice.ts";
+import React, {useMemo, useState} from 'react'
+import {UpkQuestion, UpkQuestionChoice} from "@/api";
import {Card, CardContent, CardHeader} from "@/components/ui/card.tsx";
-import {useAppSelector} from "@/hooks.ts";
+import {useAppDispatch, useAppSelector} from "@/hooks.ts";
+import {addAnswer, makeSelectChoicesByQuestion} from "@/models/answerSlice.ts";
+import {selectQuestionById} from "@/models/questionSlice.ts";
+import {useSelector} from "react-redux";
+import {Button} from "@/components/ui/button"
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+} from "@/components/ui/pagination"
const TextEntry: React.FC<{ question: UpkQuestion }> = ({question}) => {
- // const dispatch = useAppDispatch()
- // const buckets = useAppSelector(state => state.buckets)
-
- // const handleInputChange = (event: React.ChangeEvent) => {
- // // Don't allow any input changes after they triggered submission...
- // if (question._complete || question._processing) {
- // return
- // }
- //
- // // Assign the input value as an answer to the question
- // const newValue = event.target.value;
- // dispatch(setAnswer({questionId: question.questionId, val: newValue}))
- // };
+ const dispatch = useAppDispatch()
+ const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]);
+ const answer = useSelector(selectAnswer);
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ dispatch(addAnswer({question: question, val: event.target.value}))
+ };
+
+ console.log("TextEntry.answer", answer)
return (
@@ -30,83 +36,52 @@ const TextEntry: React.FC<{ question: UpkQuestion }> = ({question}) => {
id="text-entry-input"
aria-describedby=""
placeholder=""
- // onInput={handleInputChange}
+ onKeyDown={handleInputChange}
/>
- {/*{question.error_msg}*/}
+ {answer.error_msg}
)
}
const MultiChoiceItem: React.FC<{ question: UpkQuestion, choice: UpkQuestionChoice }> = ({question, choice}) => {
-
- // const onclick = function () {
- // // Assign the input value as an answer to the question
- // let answer = getAnswer;
- // let click_value = childView.model.get("choice_id")
- //
- // switch (question.selector) {
- //
- // case "SA": /// Single Answer
- // answer.set("values", [{"value": click_value}]);
- // break
- //
- // case "MA": /// Multi Answer
- // let current_values: AnswerValueItemType[] = answer.get("values") ?? [];
- //
- // if (includes(map(current_values, "value"), click_value)) {
- // // The item has already been selected
- // current_values = remove(current_values, (v) => {
- // return v["value"] != click_value
- // })
- //
- // } else {
- // // It's a new selection
- // current_values.push({"value": click_value})
- // }
- //
- // answer.set("values", current_values);
- // break
- // }
- // childView.render();
- //
- // // Validate the answer and show any information
- // let res: QuestionValidationResult = this.model.validateAnswer()
- // this.ui.message.text(res["message"]);
- // }
-
- // const render = function () {
- // let answer = this.getOption("answer");
- // let current_values: AnswerValueItemType[] = answer.get("values") ?? [];
- // let current_values_values = map(current_values, "value");
- //
- // if (includes(current_values_values, this.model.get("choice_id"))) {
- // this.$el.addClass("selected");
- // }
- // }
+ const dispatch = useAppDispatch()
+ const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]);
+ const answer = useSelector(selectAnswer);
+ const selected = answer.values.includes(choice.choice_id)
return (
- <>
+
)
}
const MultipleChoice: React.FC<{ question: UpkQuestion }> = ({question}) => {
+ const selectAnswer = useMemo(() => makeSelectChoicesByQuestion(question), [question]);
+ const answer = useSelector(selectAnswer);
return (
{question.question_text}
- {/*{question.error_msg}*/}
+ {answer.error_msg}
- {
- question.choices.map(c => {
- return
- })
- }
+
+ {
+ question.choices.map(c => {
+ return
+ })
+ }
+
)
@@ -128,6 +103,12 @@ const ProfileQuestionFull: React.FC = ({question}) => {
const QuestionsPage = () => {
const questions = useAppSelector(state => state.questions)
+ const [activeQuestionID, setQuestionID] = useState(() => questions[0].question_id);
+
+ const selectQuestion = useMemo(() => selectQuestionById(activeQuestionID), [questions]);
+ const question = useSelector(selectQuestion);
+
+ console.log(activeQuestionID, questions)
return (
@@ -135,11 +116,30 @@ const QuestionsPage = () => {
A total of {questions.length} questions are available.
- {
- questions.map(q => {
- return
;
- })
- }
+
+
+ {
+ questions.map((q, i) => {
+ return (
+
+ setQuestionID(q.question_id)}
+ >
+ {i+1}
+
+
+ )
+ })
+ }
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/store.ts b/src/store.ts
index 4bd54ca..4e56b39 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -3,12 +3,20 @@ import {configureStore} from '@reduxjs/toolkit'
import bucketReducers from "@/models/bucketSlice.ts"
import questionReducers from "@/models/questionSlice.ts"
import appReducers from "@/models/appSlice.ts"
+import answerReducers from "@/models/answerSlice.ts"
export const store = configureStore({
reducer: {
app: appReducers,
+
+ // - Read Only
+ // -- These act as API cache stores to allow background loading
buckets: bucketReducers,
- questions: questionReducers
+ questions: questionReducers,
+
+ // - Read Write
+ // -- This stores user engagement (eg: answering any questions)
+ answers: answerReducers
}
})
--
cgit v1.2.3