diff options
| -rw-r--r-- | src/Widget.tsx | 23 | ||||
| -rw-r--r-- | src/components/nav-main.tsx | 11 | ||||
| -rw-r--r-- | src/main.tsx | 4 | ||||
| -rw-r--r-- | src/models/app.ts | 4 | ||||
| -rw-r--r-- | src/models/appSlice.ts | 10 | ||||
| -rw-r--r-- | src/models/questionSlice.ts | 6 | ||||
| -rw-r--r-- | src/pages/Offerwall.tsx | 89 | ||||
| -rw-r--r-- | src/pages/Questions.tsx | 67 |
8 files changed, 167 insertions, 47 deletions
diff --git a/src/Widget.tsx b/src/Widget.tsx index c28b55d..3f7902c 100644 --- a/src/Widget.tsx +++ b/src/Widget.tsx @@ -7,9 +7,10 @@ import {QuestionsPage} from "@/pages/Questions.tsx"; import {useAppDispatch, useAppSelector} from "@/hooks.ts"; import {OfferwallApi, ProfilingQuestionsApi} from "@/api"; +import {ProfileQuestion, setQuestions} from "@/models/questionSlice.ts"; import {setBuckets} from "@/models/bucketSlice.ts"; -import {setQuestions} from "@/models/questionSlice.ts" import {CashoutMethodsPage} from "@/pages/CashoutMethods.tsx"; +import {setAvailabilityCount, setOfferwallId} from "@/models/appSlice.ts" import './index.css'; @@ -23,13 +24,21 @@ const Widget = () => { // https://fsb.generalresearch.com/{product_id}/offerwall/37d1da64/?country new OfferwallApi().offerwallSoftpairProductIdOfferwall37d1da64Get(app.bpid, app.bpuid, "104.9.125.144") .then(res => { + + // We want to set these questions first, because the Bucket Component views may do + // some redux lookups + const objects: ProfileQuestion[] = Object.values(res.data.offerwall.question_info) as ProfileQuestion[] + dispatch(setQuestions(objects)) + + dispatch(setAvailabilityCount(res.data.offerwall.availability_count)) + dispatch(setOfferwallId(res.data.offerwall.id)) dispatch(setBuckets(res.data.offerwall.buckets)) }) .catch(err => console.log(err)); - new ProfilingQuestionsApi().getProfilingQuestionsProductIdProfilingQuestionsGet(app.bpid, app.bpuid, "104.9.125.144", undefined, undefined, 2500 ) + new ProfilingQuestionsApi().getProfilingQuestionsProductIdProfilingQuestionsGet(app.bpid, app.bpuid, "104.9.125.144", undefined, undefined, 2500) .then(res => { - dispatch(setQuestions(res.data.questions)) + dispatch(setQuestions(res.data.questions as ProfileQuestion[])) }) .catch(err => console.log(err)); }, []); // ← empty array means "run once" @@ -37,7 +46,7 @@ const Widget = () => { return ( <SidebarProvider> - <AppSidebar variant="floating" /> + <AppSidebar variant="floating"/> <SidebarInset> <SiteHeader/> @@ -45,9 +54,9 @@ const Widget = () => { <div className="@container/main flex flex-1 flex-col gap-2"> <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> <div className="px-4 lg:px-6"> - {app.currentPage === 'offerwall' && <Offerwall />} - {app.currentPage === 'questions' && <QuestionsPage />} - {app.currentPage === 'cashouts' && <CashoutMethodsPage />} + {app.currentPage === 'offerwall' && <Offerwall/>} + {app.currentPage === 'questions' && <QuestionsPage/>} + {app.currentPage === 'cashouts' && <CashoutMethodsPage/>} </div> </div> </div> diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx index 34fa5a8..1f276b1 100644 --- a/src/components/nav-main.tsx +++ b/src/components/nav-main.tsx @@ -9,7 +9,7 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar" import {setPage} from "@/models/appSlice.ts"; -import {useAppDispatch} from "@/hooks.ts"; +import {useAppDispatch, useAppSelector} from "@/hooks.ts"; import {useSelector} from "react-redux"; import {selectQuestions} from "@/models/questionSlice.ts"; import {Badge} from "@/components/ui/badge" @@ -17,6 +17,7 @@ import {Badge} from "@/components/ui/badge" export function NavMain() { const dispatch = useAppDispatch() + const app = useAppSelector(state => state.app) const questions = useSelector(selectQuestions) return ( @@ -29,7 +30,13 @@ export function NavMain() { > <SidebarMenuButton tooltip="Surveys"> <NotebookText/> - <span>Surveys</span> + <span> + Surveys <Badge + className="absolute top-2 right-2 h-5 min-w-5 rounded-full px-1 font-mono tabular-nums cursor-pointer" + variant="outline" + title={`${(app.availability_count ?? 0).toLocaleString()} live surveys`} + >{(app.availability_count ?? 0).toLocaleString()}</Badge> + </span> </SidebarMenuButton> </SidebarMenuItem> diff --git a/src/main.tsx b/src/main.tsx index a6cc146..678f9a0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -42,11 +42,11 @@ import {setApp} from "@/models/appSlice.ts"; const root = createRoot(container) root.render( - <React.StrictMode> + // <React.StrictMode> <Provider store={store}> <Widget/> </Provider> - </React.StrictMode> + // </React.StrictMode> ); })()
\ No newline at end of file diff --git a/src/models/app.ts b/src/models/app.ts index a0412e0..62da7bb 100644 --- a/src/models/app.ts +++ b/src/models/app.ts @@ -10,4 +10,8 @@ export interface App { leaderboard: boolean; currentPage: Page + + //-- responses saved from the last OfferwallResponse + availability_count: number | undefined; + offerwall_id: string | undefined; } diff --git a/src/models/appSlice.ts b/src/models/appSlice.ts index 3adf316..b8c41c6 100644 --- a/src/models/appSlice.ts +++ b/src/models/appSlice.ts @@ -14,12 +14,20 @@ const appSlice = createSlice({ }, setPage(state, action: PayloadAction<Page>) { state.currentPage = action.payload; + }, + setAvailabilityCount(state, action: PayloadAction<number>) { + state.availability_count = action.payload; + }, + setOfferwallId(state, action: PayloadAction<string>) { + state.offerwall_id = action.payload; } } }) export const { setApp, - setPage + setPage, + setAvailabilityCount, + setOfferwallId } = appSlice.actions; export default appSlice.reducer
\ No newline at end of file diff --git a/src/models/questionSlice.ts b/src/models/questionSlice.ts index 9543088..a617234 100644 --- a/src/models/questionSlice.ts +++ b/src/models/questionSlice.ts @@ -58,6 +58,12 @@ export default questionSlice.reducer // We need to fetch the next available Question that either doesn't have an Answer, or the Answer // isn't Answer.completed export const selectQuestions = (state: RootState) => state.questions +export const makeSelectQuestionsByIds = (ids: string[]) => + createSelector( + (state: RootState) => state.questions, + (questions: ProfileQuestion[]) => questions.filter(q => ids.includes(q.question_id)) + ) + export const selectActiveQuestion = (state: RootState) => state.questions.find(i => i.active) export const selectAnswers = (state: RootState) => state.answers diff --git a/src/pages/Offerwall.tsx b/src/pages/Offerwall.tsx index acce696..178e762 100644 --- a/src/pages/Offerwall.tsx +++ b/src/pages/Offerwall.tsx @@ -1,12 +1,25 @@ import React from 'react' import {Separator} from "@/components/ui/separator" import {Link} from '@mui/material'; +import {useSelector} from "react-redux"; import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card.tsx"; import {ScrollArea} from "@/components/ui/scroll-area.tsx"; +import {makeSelectQuestionsByIds, setNextQuestion, setQuestions} from "@/models/questionSlice.ts" import {CheckIcon, MessageCircleQuestionIcon, XIcon} from "lucide-react" -import {SoftPairBucket} from "@/api/models/soft-pair-bucket.ts" -import {useAppSelector} from '@/hooks' +import { + BodyOfferwallSoftpairPostProductIdOfferwall37d1da64OfferwallIdPost, + OfferwallApi, + SoftPairBucket, + UserQuestionAnswerIn +} from "@/api" +import {useAppDispatch, useAppSelector} from '@/hooks' +import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger,} from "@/components/ui/sheet" +import {ProfileQuestionFull} from "@/pages/Questions.tsx" +import {Answer, saveAnswer, selectAnswerForQuestion, submitAnswer} from "@/models/answerSlice.ts"; +import {assert} from "@/lib/utils.ts"; +import {setAvailabilityCount, setOfferwallId} from "@/models/appSlice.ts"; +import {setBuckets} from "@/models/bucketSlice.ts"; const BucketStatus: React.FC<SoftPairBucket> = ({bucket}) => { switch (bucket.eligibility) { @@ -19,6 +32,74 @@ const BucketStatus: React.FC<SoftPairBucket> = ({bucket}) => { } } +const ConditionalQuestions: React.FC<SoftPairBucket> = ({bucket}) => { + const dispatch = useAppDispatch() + + const questions = useSelector(makeSelectQuestionsByIds(bucket.missing_questions)) + const question = questions[0] + const answer: Answer | undefined = useSelector(selectAnswerForQuestion(question)); + const app = useAppSelector(state => state.app) + + console.log("Conditional bucket:", questions, question, answer) + + const submitEvt = () => { + 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") + + let body: BodyOfferwallSoftpairPostProductIdOfferwall37d1da64OfferwallIdPost = { + 'answers': [{ + "question_id": question.question_id, + "answer": answer.values + } as UserQuestionAnswerIn + ] + } + + new OfferwallApi().offerwallSoftpairPostProductIdOfferwall37d1da64OfferwallIdPost(app.bpid, app.offerwall_id, app.bpuid, body) + .then(res => { + if (res.status == 200) { + dispatch(setAvailabilityCount(res.data.offerwall.availability_count)) + dispatch(setOfferwallId(res.data.offerwall.id)) + dispatch(setBuckets(res.data.offerwall.buckets)) + } else { + // let error_msg = res.data.msg + } + }) + .catch(err => console.log(err)); + } + + return ( + <Sheet> + <SheetTrigger>Open</SheetTrigger> + <SheetContent + side="right" + className="md:w-[900px], lg:w-[1000px]"> + + <SheetHeader> + <SheetTitle>Bucket Questions</SheetTitle> + <SheetDescription> + This survey has some unanswered questions. Answer these to determine if you're + eligible for the Survey Bucket + </SheetDescription> + </SheetHeader> + + { + questions.map(q => { + return <ProfileQuestionFull + key={q.question_id} + question={q} + submitAnswerEvt={submitEvt} + className="mt-4 m-2"/> + }) + } + + </SheetContent> + </Sheet> + ) +} + const CallToAction: React.FC<SoftPairBucket> = ({bucket}) => { switch (bucket.eligibility) { case "eligible": @@ -28,9 +109,7 @@ const CallToAction: React.FC<SoftPairBucket> = ({bucket}) => { </button> </Link>; case "conditional": - return <button type="button"> - Unlock Survey - </button>; + return <ConditionalQuestions bucket={bucket}/> case "ineligible": return <button type="button"> diff --git a/src/pages/Questions.tsx b/src/pages/Questions.tsx index 3cd6f71..06374b2 100644 --- a/src/pages/Questions.tsx +++ b/src/pages/Questions.tsx @@ -110,9 +110,9 @@ const MultipleChoice: React.FC<{ question: ProfileQuestion }> = ({question}) => } -const ProfileQuestionFull: React.FC<{ - question: ProfileQuestion, -}> = ({question}) => { +export const ProfileQuestionFull: React.FC<{ + question: ProfileQuestion, submitAnswerEvt: () => void +}> = ({question, submitAnswerEvt}) => { const dispatch = useAppDispatch() @@ -134,31 +134,6 @@ 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") - - let body: BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost = { - 'answers': [{ - "question_id": question.question_id, - "answer": answer.values - } as UserQuestionAnswerIn - ] - } - new ProfilingQuestionsApiFactory().submitProfilingQuestionsProductIdProfilingQuestionsPost(app.bpid, app.bpuid, body) - .then(res => { - if (res.status == 200) { - dispatch(saveAnswer({question: question})) - dispatch(setNextQuestion()) - } else { - // let error_msg = res.data.msg - } - }) - .catch(err => console.log(err)); - } return ( <Card className="@container/card relative overflow-hidden"> @@ -175,9 +150,9 @@ const ProfileQuestionFull: React.FC<{ <Badge className="absolute top-2 right-2 h-5 min-w-5 rounded-full px-1 font-mono tabular-nums cursor-pointer" variant="outline" - title={`Currently ${question.task_count.toLocaleString()} surveys use this profiling question`} + title={`Currently ${(question.task_count ?? 0).toLocaleString()} surveys use this profiling question`} > - {question.task_count.toLocaleString()} + {(question.task_count ?? 0).toLocaleString()} </Badge> <CardHeader> @@ -249,6 +224,9 @@ const QuestionsPage = () => { // cannot be done within the click handler. const nextQuestion = useSelector(selectNextAvailableQuestion) + const answer: Answer | undefined = useSelector(selectAnswerForQuestion(question)); + const app = useAppSelector(state => state.app) + const clickNext = () => { // TODO: if nextQuestion was already submitted, skip it! if (nextQuestion) { @@ -285,6 +263,34 @@ const QuestionsPage = () => { return items.slice(start, end) } + + 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") + + let body: BodySubmitProfilingQuestionsProductIdProfilingQuestionsPost = { + 'answers': [{ + "question_id": question.question_id, + "answer": answer.values + } as UserQuestionAnswerIn + ] + } + new ProfilingQuestionsApiFactory().submitProfilingQuestionsProductIdProfilingQuestionsPost(app.bpid, app.bpuid, body) + .then(res => { + if (res.status == 200) { + dispatch(saveAnswer({question: question})) + dispatch(setNextQuestion()) + } else { + // let error_msg = res.data.msg + } + }) + .catch(err => console.log(err)); + } + + return ( <> <Pagination className="mt-4 mb-4"> @@ -311,6 +317,7 @@ const QuestionsPage = () => { <ProfileQuestionFull key={question.question_id} question={question} + submitAnswerEvt={submitAnswerEvt} className="mt-4 mb-4"/> </> ) |
