aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Nanis2025-06-07 04:17:19 +0700
committerMax Nanis2025-06-07 04:17:19 +0700
commit51b1003d1e0ce43aa6c30f461d710cb09cdfc29f (patch)
tree85a33808ae2e01a3a46ffbbf4e0255b0c27caa7e
parent257bc2f85b71a8564e95a8e6ba39ab0b00e022df (diff)
downloadpanel-ui-51b1003d1e0ce43aa6c30f461d710cb09cdfc29f.tar.gz
panel-ui-51b1003d1e0ce43aa6c30f461d710cb09cdfc29f.zip
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.
-rw-r--r--src/Widget.tsx23
-rw-r--r--src/components/nav-main.tsx11
-rw-r--r--src/main.tsx4
-rw-r--r--src/models/app.ts4
-rw-r--r--src/models/appSlice.ts10
-rw-r--r--src/models/questionSlice.ts6
-rw-r--r--src/pages/Offerwall.tsx89
-rw-r--r--src/pages/Questions.tsx67
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"/>
</>
)