From 51b1003d1e0ce43aa6c30f461d710cb09cdfc29f Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Sat, 7 Jun 2025 04:17:19 +0700 Subject: Passing in onClick for FullProfileQuestion so that Profile Question and SoftPair are different. Using API models to POST to Softpair submission (with offerwall_id saved). Updating from Conditional to Ineligible buckets. Availability Count to app state. Using / exploring sidebar to show filtered questions for specific Bucket. --- src/Widget.tsx | 23 ++++++++---- src/components/nav-main.tsx | 11 +++++- src/main.tsx | 4 +- src/models/app.ts | 4 ++ src/models/appSlice.ts | 10 ++++- src/models/questionSlice.ts | 6 +++ src/pages/Offerwall.tsx | 89 ++++++++++++++++++++++++++++++++++++++++++--- src/pages/Questions.tsx | 67 +++++++++++++++++++--------------- 8 files changed, 167 insertions(+), 47 deletions(-) (limited to 'src') 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 ( - + @@ -45,9 +54,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/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() { > - Surveys + + Surveys {(app.availability_count ?? 0).toLocaleString()} + 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( - + // - + // ); })() \ 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) { state.currentPage = action.payload; + }, + setAvailabilityCount(state, action: PayloadAction) { + state.availability_count = action.payload; + }, + setOfferwallId(state, action: PayloadAction) { + 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 = ({bucket}) => { switch (bucket.eligibility) { @@ -19,6 +32,74 @@ const BucketStatus: React.FC = ({bucket}) => { } } +const ConditionalQuestions: React.FC = ({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 ( + + Open + + + + Bucket Questions + + This survey has some unanswered questions. Answer these to determine if you're + eligible for the Survey Bucket + + + + { + questions.map(q => { + return + }) + } + + + + ) +} + const CallToAction: React.FC = ({bucket}) => { switch (bucket.eligibility) { case "eligible": @@ -28,9 +109,7 @@ const CallToAction: React.FC = ({bucket}) => { ; case "conditional": - return ; + return case "ineligible": return