diff options
| author | Max Nanis | 2025-05-28 04:41:37 +0100 |
|---|---|---|
| committer | Max Nanis | 2025-05-28 04:41:37 +0100 |
| commit | 8caa77413ea372e5cbd2980a9922d701af359c04 (patch) | |
| tree | 9341e2f70fab6b2678fdff53c002954ef69c7b3e /src/models | |
| download | panel-ui-8caa77413ea372e5cbd2980a9922d701af359c04.tar.gz panel-ui-8caa77413ea372e5cbd2980a9922d701af359c04.zip | |
initial commit
Diffstat (limited to 'src/models')
| -rw-r--r-- | src/models/CashoutMethod.ts | 33 | ||||
| -rw-r--r-- | src/models/app.ts | 13 | ||||
| -rw-r--r-- | src/models/appSlice.ts | 23 | ||||
| -rw-r--r-- | src/models/bucket.ts | 32 | ||||
| -rw-r--r-- | src/models/bucketSlice.ts | 34 | ||||
| -rw-r--r-- | src/models/question.ts | 272 | ||||
| -rw-r--r-- | src/models/questionSlice.ts | 36 |
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 |
