aboutsummaryrefslogtreecommitdiff
path: root/src/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/models')
-rw-r--r--src/models/CashoutMethod.ts33
-rw-r--r--src/models/app.ts13
-rw-r--r--src/models/appSlice.ts23
-rw-r--r--src/models/bucket.ts32
-rw-r--r--src/models/bucketSlice.ts34
-rw-r--r--src/models/question.ts272
-rw-r--r--src/models/questionSlice.ts36
7 files changed, 443 insertions, 0 deletions
diff --git a/src/models/CashoutMethod.ts b/src/models/CashoutMethod.ts
new file mode 100644
index 0000000..8da88ac
--- /dev/null
+++ b/src/models/CashoutMethod.ts
@@ -0,0 +1,33 @@
+import {CashoutMethodOut} from "@/api/models/cashout-method-out.ts"
+
+export class CashoutMethod implements CashoutMethodOut {
+ id: string; // uuid
+ currency: string; // 3-letter all caps
+ originalCurrency: string; // 3-letter all caps
+ data: any;
+ description: string; // html description
+ imageUrl: string; // cloudfront cdn
+ maxValue: number; // pixels int
+ minValue: number; // pixels int
+ name: string;
+ type: string; // TANGO
+ extId: string; // U179271
+ productId?: any;
+ productUserId?: any;
+
+ constructor(data) {
+ this.originalCurrency = data.original_currency;
+ this.currency = data.currency;
+ this.data = data.data;
+ this.description = data.description;
+ this.extId = data.ext_id;
+ this.id = data.id;
+ this.imageUrl = data.image_url;
+ this.maxValue = data.max_value;
+ this.minValue = data.min_value;
+ this.name = data.name;
+ this.type = data.type;
+
+ }
+
+} \ No newline at end of file
diff --git a/src/models/app.ts b/src/models/app.ts
new file mode 100644
index 0000000..a0412e0
--- /dev/null
+++ b/src/models/app.ts
@@ -0,0 +1,13 @@
+export type Page = 'offerwall' | 'questions' | 'cashouts';
+
+export interface App {
+ targetId: string,
+ bpid: string;
+ bpuid: string;
+ offerwall: string;
+ walletMode: boolean;
+ panelName: string | null;
+ leaderboard: boolean;
+
+ currentPage: Page
+}
diff --git a/src/models/appSlice.ts b/src/models/appSlice.ts
new file mode 100644
index 0000000..d7e4a0b
--- /dev/null
+++ b/src/models/appSlice.ts
@@ -0,0 +1,23 @@
+import {createSlice, PayloadAction} from '@reduxjs/toolkit'
+import {App, Page} from "@/models/app.ts"
+
+
+const initialState = {}
+
+const appSlice = createSlice({
+ name: 'app',
+ initialState,
+ reducers: {
+ setApp(state, action: PayloadAction<App>) {
+ console.log("setApp", action.payload)
+ return action.payload;
+ },
+ setPage(state, action: PayloadAction<Page>) {
+ console.log("setPage.state", state.currentPage, action.payload)
+ state.currentPage = action.payload;
+ }
+ }
+})
+
+export const {setApp, setPage} = appSlice.actions;
+export default appSlice.reducer \ No newline at end of file
diff --git a/src/models/bucket.ts b/src/models/bucket.ts
new file mode 100644
index 0000000..a9419a6
--- /dev/null
+++ b/src/models/bucket.ts
@@ -0,0 +1,32 @@
+import {SoftPairBucket} from "@/api/models/soft-pair-bucket.ts"
+
+export class SoftPairBucketClass implements SoftPairBucket {
+ readonly id: string;
+ readonly uri: string;
+ readonly category: any | null;
+ readonly contents: any;
+ readonly eligibility: any;
+ readonly missingQuestions: any | null;
+ readonly loi: any;
+ readonly payout: any;
+
+ constructor(data: {
+ id: string,
+ uri: string,
+ category: any | null,
+ contents: any,
+ eligibility: any,
+ missingQuestions: any | null,
+ loi: any,
+ payout: any
+ }) {
+ this.id = data.id;
+ this.uri = data.uri;
+ this.category = data.category;
+ this.contents = data.contents;
+ this.eligibility = data.eligibility;
+ this.missingQuestions = data.missingQuestions;
+ this.loi = data.loi;
+ this.payout = data.payout;
+ }
+} \ No newline at end of file
diff --git a/src/models/bucketSlice.ts b/src/models/bucketSlice.ts
new file mode 100644
index 0000000..9c6aee8
--- /dev/null
+++ b/src/models/bucketSlice.ts
@@ -0,0 +1,34 @@
+import {createSlice, PayloadAction} from '@reduxjs/toolkit'
+import {SoftPairBucketClass} from "@/models/bucket.ts"
+// import {ProfilingQuestion} from "@/models/question.ts";
+import type {RootState} from '@/store'
+import {SoftPairBucket} from "@/api/models/soft-pair-bucket.ts"
+
+// Create an initial state value for the reducer, with that type
+const initialState: SoftPairBucket[] = []
+
+// Create the slice and pass in the initial state
+const bucketSlice = createSlice({
+ name: 'buckets',
+ initialState,
+ reducers: {
+ setBuckets(state, action: PayloadAction<SoftPairBucket[]>) {
+ return action.payload;
+ },
+ bucketAdded(state, action: PayloadAction<SoftPairBucket>) {
+ state.push(action.payload);
+ }
+ }
+})
+
+// Export the generated reducer function
+export const {setBuckets, bucketAdded} = bucketSlice.actions;
+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) =>
+ state.buckets.find(bucket => bucket.id === bucketId) \ No newline at end of file
diff --git a/src/models/question.ts b/src/models/question.ts
new file mode 100644
index 0000000..996589b
--- /dev/null
+++ b/src/models/question.ts
@@ -0,0 +1,272 @@
+import {stringify} from "querystring";
+import {ChoiceType, ConfigurationType, PatternType, QuestionType, SelectorType, ValidationType} from "@/types.ts"
+import {UpkQuestion} from "@/api/models/upk-question.ts"
+import {UpkQuestionType} from "@/api";
+
+
+export class ProfilingAnswer {
+ // let values: Array<{ value: string }> = question_answer.values || []
+
+ questionId: string;
+ values: string[] = [];
+
+ constructor(qid: string, values: string[]) {
+ this.questionId = qid;
+ this.values = values
+ }
+}
+
+export class ProfilingQuestion implements UpkQuestion {
+ questionId: string; // It's a UUID
+ countryIso: string; // 2-letter lower
+ languageIso: string; // 3-letter lower
+
+ questionType: UpkQuestionType;
+ selector: SelectorType;
+ questionText: string; // "title" of the question
+
+ choices?: ChoiceType[];
+
+ extQuestionId?: any;
+ configuration: ConfigurationType | null = null;
+ validation: ValidationType | null = null;
+
+ importance?: any;
+ task_count: number;
+ task_score: number;
+
+ private _complete: boolean = false;
+ private _processing: boolean = false;
+ private _answer: ProfilingAnswer | null = null;
+
+ error_msg: string = "";
+
+ constructor(data) {
+ this.questionId = data.question_id;
+ this.countryIso = data.country_iso;
+ this.languageIso = data.language_iso;
+
+ this.questionType = data.question_type;
+ this.selector = data.selector;
+ this.questionText = data.question_text;
+
+ this.choices = data.choices
+
+ this.extQuestionId = data.ext_question_id;
+ this.configuration = data.configuration;
+ this.validation = data.validation;
+ }
+
+ // --- Properties ---
+
+ getType(): QuestionType {
+ return this.questionType as QuestionType
+ }
+
+ getSelector(): SelectorType {
+ return this.selector as SelectorType
+ }
+
+ getChoices(): ChoiceType[] | null {
+ const choices: ChoiceType[] = this.choices;
+ if (choices.length > 0) {
+ return choices;
+ } else {
+ return null;
+ }
+ }
+
+ // --- Methods ---
+
+ addAnswer(val: string): null {
+ val = val.trim();
+
+ switch (this.getType()) {
+
+ case "TE":
+ this._answer = new ProfilingAnswer(this.questionId, [val]);
+ break
+
+ case "MC":
+ let current_values: string[] = this._answer?.values
+ current_values.push(val)
+ this._answer = new ProfilingAnswer(this.questionId, current_values);
+ break
+
+ default:
+ throw new Error("Incorrect Question Type provided");
+ }
+
+ this.validate()
+ }
+
+ removeAnswer(val: string): null {
+ switch (this.getType()) {
+
+ // You can only remove a value from a MultiChoice
+ case "MC":
+ // TODO: implement this
+ // let current_values: string[] = this._answer?.values
+ // current_values.push(val)
+ // this._answer = new ProfilingAnswer(this.questionId, current_values);
+ break
+
+ default:
+ throw new Error("Incorrect Question Type provided");
+ }
+
+ this.validate()
+ }
+
+ validate(): boolean {
+ /*
+ If the question is MC, validate:
+ - validate selector SA vs MA (1 selected vs >1 selected)
+ - the answers match actual codes in the choices
+ - validate configuration.max_select
+ - validate choices.exclusive
+
+ If the question is TE, validate that:
+ - configuration.max_length
+ - validation.patterns
+ */
+
+ if (this._answer == null) {
+ this.error_msg = "An answer is required"
+ return false
+ }
+
+ let qa: ProfilingAnswer = this._answer;
+
+ switch (this.getType()) {
+ case "TE":
+ if (qa.values.length == 0) {
+ this.error_msg = "An answer is required"
+ return false
+ }
+
+ if (qa.values.length > 1) {
+ this.error_msg = "Only one answer allowed"
+ return false
+ }
+
+ let answer: string = qa.values[0]
+
+ if (answer.length <= 0) {
+ this.error_msg = "Must provide answer"
+ return false
+ }
+
+ const max_length: number = (this.configuration ?? {})["max_length"] ?? 100000
+
+ if (answer.length > max_length) {
+ this.error_msg = "Answer longer than allowed"
+ return false
+ }
+
+ const patterns: PatternType[] = (this.validation ?? {})["patterns"] ?? []
+
+ patterns.forEach((pattern) => {
+ let re = new RegExp(pattern["pattern"])
+ if (answer.search(re) == -1) {
+ this.error_msg = pattern["message"]
+ return false
+ }
+ })
+
+ this.error_msg = ""
+ return true
+
+ case "MC":
+ // if (qa.values.length == 0) {
+ // this.error_msg = "MC question with no selected answers"
+ // return false
+ // }
+ //
+ // const choice_codes = map(this.getChoices().toJSON(), "choice_id")
+ //
+ // switch (this.getSelector()) {
+ // case "SA":
+ // if (qa.values.length > 1) {
+ // this.error_msg = "Single Answer MC question with >1 selected answers"
+ // return false
+ // }
+ // break
+ // case "MA":
+ // if (qa.values.length > choice_codes.length) {
+ // this.error_msg = "More options selected than allowed"
+ // return false
+ // }
+ // 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
+ // }
+
+ /*
+ exclusive_choice = next((x for x in question["choices"] if x.get("exclusive")), None)
+ if exclusive_choice:
+ exclusive_choice_id = exclusive_choice["choice_id"]
+ assert answer == [exclusive_choice_id] or \
+ exclusive_choice_id not in answer, "Invalid exclusive selection"
+ */
+
+ this.error_msg = ""
+ return true
+
+ default:
+ throw new Error("Incorrect Question Type provided");
+ }
+
+ }
+
+ // -- Database / Format --
+ static fromJson(json: any): ProfilingQuestion {
+ return new ProfilingQuestion(json);
+ }
+
+ save() {
+ let question: ProfilingQuestion = this;
+ // @ts-ignore
+ let answer: ProfilingAnswer = question._answer;
+
+ if (this._complete || this._processing) {
+ return
+ }
+ this._processing = true
+
+ let res = JSON.stringify({
+ "answers": [{
+ "question_id": answer.get('question_id'),
+ "answer": map(answer.get("values"), "value")
+ }]
+ });
+
+ $.ajax({
+ url: ["https://fsb.generalresearch.com", questions.BPID, "profiling-questions", ""].join("/") + "?" + stringify({"bpuid": questions.BPUID}),
+ xhrFields: {withCredentials: false},
+ processData: false,
+ type: "POST",
+ contentType: "application/json; charset=utf-8",
+ data: res,
+ success: function (data) {
+ channel.trigger("ProfilingQuestions:start");
+ },
+ error: function (data) {
+ channel.trigger("ProfilingQuestions:start");
+ Sentry.captureMessage("Profiling Question submission failed.");
+ }
+ });
+ }
+
+} \ No newline at end of file
diff --git a/src/models/questionSlice.ts b/src/models/questionSlice.ts
new file mode 100644
index 0000000..6a4da6f
--- /dev/null
+++ b/src/models/questionSlice.ts
@@ -0,0 +1,36 @@
+import {createSlice, PayloadAction} from '@reduxjs/toolkit'
+import type {RootState} from '@/store'
+import {UpkQuestion} from "@/api/models/upk-question.ts"
+
+const initialState: UpkQuestion[] = []
+
+// Create the slice and pass in the initial state
+const questionSlice = createSlice({
+ name: 'questions',
+ initialState,
+ reducers: {
+ setQuestions(state, action: PayloadAction<UpkQuestion[]>) {
+ return action.payload;
+ },
+ questionAdded(state, action: PayloadAction<UpkQuestion>) {
+ 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 selectQuestionById = (state: RootState, questionId: string) => state.questions.find(q => q.id === questionId) \ No newline at end of file