From 3eaa56f0306ead818f64c3d99fc6d230d9b970a4 Mon Sep 17 00:00:00 2001
From: Max Nanis
Date: Wed, 18 Feb 2026 20:42:03 -0500
Subject: HERE WE GO, HERE WE GO, HERE WE GO
---
jb-ui/src/pages/Home.tsx | 13 +
jb-ui/src/pages/Preview.tsx | 288 +++++++++++++++
jb-ui/src/pages/Result.tsx | 123 +++++++
jb-ui/src/pages/Work.tsx | 765 +++++++++++++++++++++++++++++++++++++++
jb-ui/src/pages/max-and-lulu.jpg | Bin 0 -> 774803 bytes
5 files changed, 1189 insertions(+)
create mode 100644 jb-ui/src/pages/Home.tsx
create mode 100644 jb-ui/src/pages/Preview.tsx
create mode 100644 jb-ui/src/pages/Result.tsx
create mode 100644 jb-ui/src/pages/Work.tsx
create mode 100644 jb-ui/src/pages/max-and-lulu.jpg
(limited to 'jb-ui/src/pages')
diff --git a/jb-ui/src/pages/Home.tsx b/jb-ui/src/pages/Home.tsx
new file mode 100644
index 0000000..5a5c953
--- /dev/null
+++ b/jb-ui/src/pages/Home.tsx
@@ -0,0 +1,13 @@
+import max_lulu from "./max-and-lulu.jpg"
+
+const Home = function () {
+ return (
+
+ )
+};
+
+export default Home;
\ No newline at end of file
diff --git a/jb-ui/src/pages/Preview.tsx b/jb-ui/src/pages/Preview.tsx
new file mode 100644
index 0000000..9506664
--- /dev/null
+++ b/jb-ui/src/pages/Preview.tsx
@@ -0,0 +1,288 @@
+import {
+ Leaderboard,
+ LeaderboardApi,
+ LeaderboardCode,
+ LeaderboardFrequency,
+ LeaderboardRow,
+} from "@/api_fsb";
+import { useAppSelector } from "@/hooks";
+import { bpid, formatCentsToUSD, truncate, } from "@/lib/utils";
+import { activeSurveys, activeUsers, maxPayout } from "@/models/grlStatsSlice";
+import { clsx } from "clsx";
+import moment from 'moment';
+import { useEffect, useState } from 'react';
+
+const showSmartRank = (
+ item: LeaderboardRow,
+ index: number,
+ items: LeaderboardRow[] | undefined
+): boolean => {
+ /**
+ * Smart rank calculation - this determines if we should show the rank
+ * value. It's confusing to show people that multiple individuals ranked
+ * in the same place, so this will only display the next ranked value, eg:
+ *
+ * - 1st
+ * - ___
+ * - 3rd
+ * - ___
+ * - ___
+ * - 6th
+ *
+ * @returns number
+ */
+ if (!items) return false;
+
+ const thisRank = item.rank;
+ if (index >= 1) {
+ const prevRank = items[index - 1].rank;
+ if (thisRank === prevRank) {
+ return false;
+ }
+ }
+ return true;
+};
+
+interface FrequencyButtonsProps {
+ selected: LeaderboardFrequency;
+ onChange: (frequency: LeaderboardFrequency) => void;
+}
+
+
+const ThreeButtonToggle = ({ selected, onChange }: FrequencyButtonsProps) => {
+ const buttons = [
+ { label: 'Day', value: LeaderboardFrequency.Daily },
+ { label: 'Week', value: LeaderboardFrequency.Weekly },
+ { label: 'Month', value: LeaderboardFrequency.Monthly },
+ ];
+
+ // rounded-l
+
+ return (
+
+ {buttons.map((button, idx) => (
+ onChange(button.value)}
+ className={clsx(
+ "px-2 py-1 text-[10px] font-medium",
+ "border-t border-b border-gray-200",
+ "hover:cursor-pointer hover:bg-zinc-200 hover:text-zinc-950",
+ idx === 0 && "rounded-l",
+ idx === buttons.length - 1 && "rounded-r",
+ selected === button.value
+ ? 'text-zinc-50 bg-zinc-400'
+ : 'text-zinc-950 bg-zinc-50',
+ )}>
+ {button.label}
+
+ ))}
+
+ );
+};
+
+
+
+// Leaderboards Component
+export const Leaderboards = () => {
+ /**
+ * Leaderboards do not do any caching, or better state management. This is okay because
+ * the API is handled to take it. However, it can easily handled better client
+ * side rather than making the
+ * **/
+
+ const [frequency, setFrequency] = useState(
+ LeaderboardFrequency.Daily
+ );
+
+ const [payouts, setPayouts] = useState(null);
+ const [completes, setCompletes] = useState(null);
+
+ // const bpuid = useAppSelector(state => state.app.bpuid)
+ const bpuid = undefined
+
+ useEffect(() => {
+ new LeaderboardApi().timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(
+ bpid,
+ LeaderboardCode.CompleteCount,
+ frequency,
+ "us",
+ bpuid, // bpuid
+ undefined, // within_time
+ 10)
+ .then(res => {
+ setCompletes(res.data.leaderboard as Leaderboard)
+ })
+
+ new LeaderboardApi().timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(
+ bpid,
+ LeaderboardCode.SumUserPayout,
+ frequency,
+ "us",
+ bpuid,
+ undefined,
+ 10)
+ .then(res => {
+ setPayouts(res.data.leaderboard as Leaderboard)
+ })
+ }, [frequency]);
+
+ let dateRange = " - "
+ if (completes) {
+ const start = moment(completes?.start_timestamp * 1000).format("MMM Do, ha");
+ const end = moment(completes?.end_timestamp * 1000).format("MMM Do, h:mm:ssa");
+ //dateRange = `${start} – ${end} (${completes?.timezone_name})`;
+ dateRange = `${start} – ${end}`;
+ }
+
+ return (
+
+
+ {/* Left spacer (hidden on small screens) */}
+
+
+ {/* Centered text */}
+
+ Leaderboards
+
+
+ {/* Right-aligned button group */}
+
+
+
+
+
+
+
+
+
+
+ Most earned in the past week.
+
+
+
+ Rank
+ Bonus
+ User
+
+
+
+
+ {payouts?.rows?.map((dataItem: LeaderboardRow) => (
+
+
+ {dataItem.rank}
+
+
+ {formatCentsToUSD(dataItem.value)}
+
+
+ {truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
+
+
+ ))}
+
+
+
+
+
+ Top completes in the past week.
+
+
+
+ Rank
+ Completes
+ User
+
+
+
+
+ {completes?.rows?.map((dataItem: LeaderboardRow, index: number) => (
+
+
+ {dataItem.rank ?? showSmartRank(dataItem, index, completes?.rows)}
+
+
+ {dataItem.value}
+
+
+ {truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+
+
+// Preview Component
+const Preview = () => {
+ /**
+ *
+ * @returns {JSX.Element}
+ */
+
+ const users = useAppSelector(state => activeUsers(state))
+ const users_f: string = users?.toLocaleString() ?? " – "
+
+ const surveys = useAppSelector(state => activeSurveys(state))
+ const surveys_f: string = surveys?.toLocaleString() ?? " – "
+
+ const survey_max_cpi = useAppSelector(state => maxPayout(state))
+ const formatter = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ });
+ const survey_max_cpi_f: string = formatter.format((survey_max_cpi ?? 0) / 100)
+
+ return (
+
+
+
+
+ {users_f} people are actively attempting {surveys_f} available surveys right now.
+
+
+
+
+
+
+
+ Get paid to take surveys. Our algorithm attempts to pair you with the
+ highest paying survey available at the moment (right now, the highest
+ paying survey is {survey_max_cpi_f}) that you are eligible for,
+ each attempt is a valid HIT. Try as many as you can, our system will
+ self regulate you: we want to ensure you have a high chance of being
+ paired with a survey based on what surveys are available, as their
+ availability constantly changes.
+
+ Each HIT is paid when you attempt to take a survey and if you complete
+ the survey, you will be awarded a bonus equal to that in which the
+ survey pays out.
+
+
+ );
+};
+export default Preview;
\ No newline at end of file
diff --git a/jb-ui/src/pages/Result.tsx b/jb-ui/src/pages/Result.tsx
new file mode 100644
index 0000000..47af248
--- /dev/null
+++ b/jb-ui/src/pages/Result.tsx
@@ -0,0 +1,123 @@
+import { useAppSelector } from "@/hooks";
+import confetti from "canvas-confetti";
+import { useEffect } from "react";
+
+// getOutcomeStr: function () {
+// return {
+// 0: "UNKNOWN", 1: "ENTER",
+// 2: "INCOMPLETE", 3: "COMPLETE"
+// }[this.get('status')] || '';
+// }
+
+export const Complete = () => {
+ const ts = useAppSelector(state => state.app.taskStatus)
+
+ return (
+ <>
+ Survey Completed
+ Congratulations! You completed the Survey.
+ {ts?.user_payout_string}
+ You may close this tab and return to submit the HIT
+ >
+ )
+}
+
+export const Enter = () => {
+ return (
+ <>
+ Entered
+ You're currently in the Survey
+ You may close this tab and return to submit the HIT
+ >
+ )
+}
+
+export const InComplete = () => {
+ const ts = useAppSelector(state => state.app.taskStatus)
+ if (!ts) { return null; }
+
+ return (
+ <>
+ HIT Complete
+ Survey terminated the attempt.
+ You may close this tab and return to submit the HIT
+ >
+
+ )
+}
+
+export const Unknown = () => {
+
+ return (
+ <>
+ Task Issue
+ You're not currently eligible for any Surveys
+ You may close this tab and return to submit the HIT
+ >
+ )
+}
+
+export const Loading = () => {
+
+ return (
+ Loading...
+ )
+}
+
+
+const Result = () => {
+
+ const ts = useAppSelector(state => state.app.taskStatus)
+
+ const confettiCannons = () => {
+ const end = Date.now() + 3 * 1000
+ const colors = ["#84cc16", "#1e3a8a", "#ec4899"]
+ const frame = () => {
+ if (Date.now() > end) return
+ confetti({
+ particleCount: 3,
+ angle: 60,
+ spread: 55,
+ startVelocity: 80,
+ gravity: 0.7,
+ origin: { x: 0, y: 0.8 },
+ colors: colors,
+ })
+ confetti({
+ particleCount: 3,
+ angle: 120,
+ spread: 55,
+ startVelocity: 80,
+ gravity: 0.7,
+ origin: { x: 1, y: 0.8 },
+ colors: colors,
+ })
+ requestAnimationFrame(frame)
+ }
+ frame()
+ }
+
+ useEffect(() => {
+ if (ts?.status === 3) {
+ confettiCannons()
+ }
+ }, [ts])
+
+
+ switch (ts?.status) {
+ case 0: // UNKNOWN
+ return ;
+ case 1: // ENTER
+ return ;
+ case 2: // INCOMPLETE
+ return ;
+ case 3: // COMPLETE
+ return ;
+ default:
+ return ;
+ }
+
+};
+
+
+export default Result;
\ No newline at end of file
diff --git a/jb-ui/src/pages/Work.tsx b/jb-ui/src/pages/Work.tsx
new file mode 100644
index 0000000..c54732f
--- /dev/null
+++ b/jb-ui/src/pages/Work.tsx
@@ -0,0 +1,765 @@
+import {
+ BucketTask,
+ OfferwallApi,
+ OfferwallReason,
+ ReportApi, ReportTask, ReportValue, StatusApi,
+ TasksStatusResponse, TaskStatusResponse, TopNPlusBlockOfferWall
+} from "@/api_fsb";
+
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Field,
+ FieldDescription,
+ FieldGroup,
+ FieldLabel,
+ FieldLegend,
+ FieldSet,
+} from "@/components/ui/field";
+import { useAppDispatch, useAppSelector } from "@/hooks";
+import { bpid, formatCentsToUSD, formatSource, truncate } from "@/lib/utils";
+import {
+ getAvailabilityCount, getLOIText,
+ isLowBalance,
+ selectBucket,
+ setAttemptedLiveEligibleCount,
+ setAvailabilityCount,
+ setCurrentBuckets,
+ setEnteredTimestamp, setLOI,
+ setOfferwallReasons,
+ setTaskStatus
+} from "@/models/appSlice";
+import {
+ createColumnHelper, flexRender, getCoreRowModel,
+ useReactTable
+} from "@tanstack/react-table";
+import { XIcon } from "lucide-react";
+import moment from "moment/moment";
+import { useEffect, useRef, useState } from "react";
+import Markdown from 'react-markdown';
+
+const Report = ({ onClose }: { onClose: () => void }) => {
+ const [showComplete, setShowComplete] = useState(false);
+ const [selectedValues, setSelectedValues] = useState([]);
+
+ const bpuid = useAppSelector(state => state.app.bpuid)
+ const ts = useAppSelector(state => state.app.taskStatus)
+
+ const handleCheckboxChange = (value: ReportValue, checked: boolean) => {
+ setSelectedValues(prev =>
+ checked
+ ? [...prev, value]
+ : prev.filter(v => v !== value)
+ );
+ };
+
+ const clicked_submit = () => {
+ if (!bpuid) return;
+ // ReportValue.NUMBER_0
+ console.log("selectedValues:", selectedValues);
+
+ const rt: ReportTask = {
+ 'bpuid': bpuid,
+ 'reasons': selectedValues,
+ 'notes': ""
+ };
+
+ new ReportApi().reportTaskProductIdReportPost(
+ bpid, // productId
+ rt, // reportTask
+ ).then(res => {
+ console.log("Response:", res); // Check what's actually here
+
+ if (res.status === 400) {
+ // Handle error here instead
+ } else {
+ setShowComplete(true);
+ }
+ })
+ }
+
+ const reportReasons = [
+ { id: 'TECHNICAL_ERROR', label: 'Technical Error', value: ReportValue.NUMBER_1 },
+ { id: 'NO_REDIRECT', label: 'No Redirect', value: ReportValue.NUMBER_2 },
+ { id: 'PRIVACY_INVASION', label: 'Privacy Invasion', value: ReportValue.NUMBER_3 },
+ { id: 'UNCOMFORTABLE_TOPICS', label: 'Uncomfortable Topics', value: ReportValue.NUMBER_4 },
+ { id: 'ASKED_FOR_NOT_ALLOWED_ACTION', label: 'Asked for Not Allowed Action', value: ReportValue.NUMBER_5 },
+ { id: 'BAD_ON_MOBILE', label: 'Bad on Mobile', value: ReportValue.NUMBER_6 },
+ { id: 'DIDNT_LIKE', label: "Didn't Like", value: ReportValue.NUMBER_7 },
+ ];
+
+ if (!ts?.finished) {
+ return Please finish a Task to report its status.
+ }
+
+ if (showComplete) {
+ return
+ }
+
+ return (
+
+
+
+
+ Please select all reasons why you are reporting this Task:
+
+
+ You must provide at least one reason to report a Task.
+
+
+
+ {reportReasons.map((reason) => (
+
+
+ handleCheckboxChange(reason.value, checked as boolean)
+ }
+ />
+
+ {reason.label}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Submit and Complete HIT
+
+
+ )
+}
+
+const SubimtAMT = () => {
+ const bpuid = useAppSelector(state => state.app.bpuid)
+ const assignment_id = useAppSelector(state => state.app.assignment_id)
+ const ts = useAppSelector(state => state.app.taskStatus)
+ const turkSubmitTo = useAppSelector(state => state.app.turkSubmitTo)
+
+ const formRef = useRef(null);
+
+ useEffect(() => {
+ if (!ts) return;
+ if (!bpuid) return;
+ if (!assignment_id) return;
+ if (!turkSubmitTo) return;
+
+ if (formRef.current) {
+ formRef.current.submit();
+ }
+ }, [ts?.tsid, bpuid, assignment_id, turkSubmitTo]);
+
+ return (
+ <>
+
+ >
+ )
+}
+
+
+const SurveyMonitor = () => {
+ const dispatch = useAppDispatch()
+
+ const [isPolling, setIsPolling] = useState(true);
+ // const [timeAgo, setTimeAgo] = useState("");
+
+ const [showComplete, setShowComplete] = useState(false);
+ const [showReport, setShowReport] = useState(false);
+
+ const bpuid = useAppSelector(state => state.app.bpuid)
+ const entered = useAppSelector(state => state.app.currentBucketEntered)
+ const isLow = useAppSelector(state => isLowBalance(state))
+
+ const ts = useAppSelector(state => state.app.taskStatus)
+ const isBaddie = ts?.status_code_1 === "SESSION_START_FAIL" || ts?.status_code_1 === "SESSION_START_QUALITY_FAIL" || ts?.status_code_1 === "SESSION_CONTINUE_QUALITY_FAIL";
+
+ // useEffect(() => {
+ // if (!ts) return;
+
+ // const updateTimeAgo = () => {
+ // const time_ago = moment(ts.started).fromNow();
+ // setTimeAgo(time_ago);
+ // };
+
+ // // Update immediately
+ // updateTimeAgo();
+
+ // // Then update every minute
+ // const intervalId = setInterval(updateTimeAgo, 60 * 1000);
+
+ // return () => clearInterval(intervalId);
+ // }, [ts]);
+
+ useEffect(() => {
+ if (!bpuid) return;
+ if (!entered) return;
+ if (!isPolling) return;
+
+ const pollApi = async () => {
+ new StatusApi().listTaskStatusesProductIdStatusGet(
+ bpid, // productId
+ bpuid, // Worker ID
+ entered, // startedAfter
+ moment.utc().unix() // startedBefore
+ ).then(res => {
+ const d = res.data as TasksStatusResponse;
+
+ if (!d.tasks_status || d.tasks_status.length === 0) {
+ // Likely still in a GRS survey
+ return;
+ }
+
+ // if (d_len >= 2) Sentry.captureMessage("Results returned back multiple")
+ const ts = d.tasks_status![0] as TaskStatusResponse;
+ dispatch(setTaskStatus(ts))
+
+ if (ts.status == 2 || ts.status == 3) {
+ // INCOMPLETE + COMPLETE
+ setIsPolling(false);
+ }
+ });
+
+ // If there is already a TaskStatus, update the time ago
+ // if (ts) {
+ // const time_ago = moment(ts.started).fromNow();
+ // setTimeAgo(time_ago);
+ // }
+ };
+
+ // Call immediately on mount, then set up interval
+ pollApi();
+ const intervalId = setInterval(pollApi, 4 * 1000);
+
+ // Cleanup: stop interval when component unmounts or isPolling changes
+ return () => clearInterval(intervalId);
+ }, [isPolling, entered]);
+
+ if (!ts) {
+ return (
+ Loading
+ )
+ }
+
+ // This is if they are actively in a survey or we can't determine the
+ // status yet. We want to show the timer and keep them on this page to
+ // monitor for completion. This gets shown no matter what regardless of
+ // balance.
+ //
+ // 0 - UNKNOWN, 1 - ENTER
+ if (ts.status === 0 || ts.status === 1) {
+ return (
+ <>
+
+
+ In Progress
+
+
+ We're actively monitoring your Task status.
+
+
+ >
+ )
+ }
+
+ // This is if they have finished a survey complete and it resulted in a
+ // COMPLETE or a INCOMPLETE (doesn't matter). They can now submit the HIT.
+ //
+ // 2 - INCOMPLETE, 3 - COMPLETE
+ return (
+ <>
+
+
+ {ts.status === 3 ? "Survey Complete!" : "Survey Terminated."}
+
+
+ {((ts.status === 3 || !isLow) && !isBaddie) && (
+
+ You may now submit the HIT.
+
+ )}
+
+
+ {(isBaddie) ? (
+ <>
+
+
+ Quality Check Failed.
+
+
+ Unfortunately, it looks like you did not pass the quality check for this survey.
+
+
+
+ Please return the HIT to avoid having your assignment rejected.
+
+
+ >
+
+ ) : (
+ <>
+ {(ts.status === 3 || !isLow) ? (
+ <>
+
+
+ setShowComplete(true)}
+ >Submit HIT
+
+ setShowReport(!showReport)}
+ >Report Survey
+
+
+ {showComplete && }
+ {showReport && setShowReport(false)} />}
+ >
+ ) : (
+
+
+ Your balance is low. You cannot submit the HIT at this time.
+
+
+
+ Please return the HIT to avoid having your assignment rejected.
+
+
+ )}
+ >
+
+ )}
+
+
+ >
+ )
+}
+
+
+const LOISelect = () => {
+ // This is what's responsible for being able to click on the text
+ // we set the global LOI times.
+
+ const dispatch = useAppDispatch()
+ const loi: number = useAppSelector(state => state.app.loi)
+ const bucket = useAppSelector(state => selectBucket(state))
+
+ const toggle_loi = function () {
+ dispatch(setLOI(loi >= 1800 ? 600 : loi + 600))
+ }
+
+ const time_f = useAppSelector(state => getLOIText(state))
+
+ const avg_time_f = ((bucket?.duration.q2 ?? 0) / 60).toLocaleString(
+ 'en-US', { minimumFractionDigits: 0, maximumFractionDigits: 1 })
+
+ const formatter = new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ });
+ const avg_pay_f: string = formatter.format((bucket?.payout.q2 ?? 0) / 100)
+
+ return (
+
+
+ I have {time_f} minutes
+
+
+ The average time spent is {avg_time_f} minutes and pays {avg_pay_f}
+
+ )
+
+};
+
+const SurveyLoading = () => {
+ return (
+
+ )
+}
+
+
+const NoSurveys = () => {
+ const offerwall_reasons = useAppSelector(state => state.app.offerwall_reasons)
+
+ const REASON_DISPLAY: Record = {
+ [OfferwallReason.UserBlocked]: {
+ label: "Account Restricted",
+ description: "Your account has been restricted.",
+ },
+ [OfferwallReason.HighReconRate]: {
+ label: "High Reconciliation Rate",
+ description: "Your account has an unusually high reconciliation rate.",
+ },
+ [OfferwallReason.UncommonDemographics]: {
+ label: "Demographic Mismatch",
+ description: "No surveys require your current demographics.",
+ },
+ [OfferwallReason.UnderMinimumAge]: {
+ label: "Age Requirement Not Met",
+ description: "You must meet the minimum age requirement to participate.",
+ },
+ [OfferwallReason.ExhaustedHighValueSupply]: {
+ label: "No Premium Offers Available",
+ description: "You've attempted all premium surveys.",
+ },
+ [OfferwallReason.AllEligibleAttempted]: {
+ label: "All Offers Attempted",
+ description: "You've attempted all available surveys.",
+ },
+ [OfferwallReason.LowCurrentSupply]: {
+ label: "Limited Availability",
+ description: "There are currently limited opportunities available.",
+ },
+ };
+
+ function formatReasons(reasons: OfferwallReason[]): { label: string; description: string }[] {
+ return reasons.map((r) => REASON_DISPLAY[r]);
+ }
+
+ return (
+ <>
+
+
+ No Surveys available…
+
+
+
+ {formatReasons(offerwall_reasons).map(r => {
+ return (
+
+ )
+ })}
+ >
+ )
+}
+
+const BucketDetails = () => {
+ const bucket = useAppSelector(state => selectBucket(state))
+ if (!bucket) return null;
+ const [showDetails, setShowDetails] = useState(false);
+
+ const columnHelper = createColumnHelper()
+ const columns = [
+ columnHelper.display({
+ id: 'index',
+ header: '#',
+ cell: ({ row }) => row.index + 1,
+ size: 25,
+ meta: {
+ align: 'center'
+ }
+ }),
+ columnHelper.accessor('source', {
+ header: () => 'Marketplace',
+ cell: (props) => {
+ const s = props.getValue()
+ return formatSource(s)
+ },
+ size: 85,
+ meta: {
+ align: 'left'
+ }
+ }),
+ columnHelper.accessor('id', {
+ header: () => 'Survey ID',
+ cell: props => {
+ return truncate(props.getValue(), 6, "*****").toUpperCase()
+ },
+ size: 70,
+ meta: {
+ align: 'left'
+ }
+ }),
+ columnHelper.accessor('payout', {
+ header: () => 'Amount',
+ cell: (props) => {
+ const val = props.renderValue() as number;
+ return formatCentsToUSD(val)
+ },
+ size: 50,
+ meta: {
+ align: 'center'
+ }
+ }),
+ columnHelper.accessor('loi', {
+ header: () => 'Duration',
+ cell: (props) => {
+ const val = props.renderValue() as number;
+ return `${Math.round(val / 60)} min`
+ },
+ size: 50,
+ meta: {
+ align: 'center'
+ }
+ }),
+ ]
+
+ const table = useReactTable({
+ 'data': bucket.contents,
+ 'columns': columns,
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ return (
+ <>
+ setShowDetails(!showDetails)}
+ >
+ ({showDetails ? 'Hide' : 'Show'} details of surveys in this bucket)
+
+
+ {showDetails && (
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+
+ {headerGroup.headers.map((header) => {
+ const align = header.column.columnDef.meta?.align || 'left';
+ const alignClass = align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left';
+
+ return (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ )
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => {
+ const align = cell.column.columnDef.meta?.align || 'left';
+ const alignClass = align === 'center' ? 'text-center' :
+ align === 'right' ? 'text-right' : 'text-left';
+
+ return (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ )
+ })}
+
+ ))}
+
+
+
+
+ )}
+ >
+ )
+};
+
+const SurveyEnter = () => {
+ const dispatch = useAppDispatch()
+ const loi = useAppSelector(state => state.app.loi)
+ const bpuid = useAppSelector(state => state.app.bpuid)
+
+ const available_cnt = useAppSelector(state => getAvailabilityCount(state))
+ const buckets = useAppSelector(state => state.app.currentBuckets)
+ const bucket = useAppSelector(state => selectBucket(state))
+ const isLow = useAppSelector(state => isLowBalance(state))
+
+ useEffect(() => {
+ if (!bpuid) return;
+
+ new OfferwallApi().topNPlusBlockOfferwallProductIdOfferwallD48cce47Get(
+ bpid, // productId
+ bpuid, // bpuid
+ undefined, // ip
+ undefined, // countryIso
+ undefined, // languages
+ undefined, // behavior
+ undefined, // minPayout
+ loi, //duration
+ 1 // nBins
+ ).then(res => {
+ const top_n_res = res.data.offerwall as TopNPlusBlockOfferWall;
+ dispatch(setAvailabilityCount(top_n_res.availability_count))
+ if (top_n_res.attempted_live_eligible_count) {
+ dispatch(setAttemptedLiveEligibleCount(top_n_res.attempted_live_eligible_count))
+ }
+ dispatch(setCurrentBuckets(top_n_res.buckets))
+ dispatch(setOfferwallReasons(top_n_res.offerwall_reasons ?? []))
+ });
+ }, [loi, bpuid]);
+
+
+ const start_survey = () => {
+ if (!bucket?.uri) return;
+
+ dispatch(setEnteredTimestamp());
+ window.open(bucket.uri, '_blank');
+ }
+
+ if (buckets === undefined) {
+ return
+ }
+
+ if (available_cnt === 0 || buckets.length === 0 || !bucket) {
+ return
+ }
+
+ return (
+ <>
+ {isLow && (
+
+
+ Your balance is low.
+
+
+ You may continue to attempt surveys, but you will be
+ advised to return the HIT if your survey attempt does
+ not result in a complete.
+
+
+ HIT submissions with a balance exceeding -$1.00 will
+ return in rejected assignments.
+
+
+ )}
+
+
+
+
+
+
+
+
+ {bucket.eligibility_explanation}
+
+
+
+ >
+ )
+};
+
+
+const Work = () => {
+ const entered = useAppSelector(state => state.app.currentBucketEntered)
+ const assignment_id = useAppSelector(state => state.app.assignment_id)
+
+ if (!assignment_id) {
+ return (
+
+ No Assignment ID found. Please access this page through
+ the Amazon Mechanical Turk platform.
+
+ )
+ }
+
+ return (
+ <>
+ {entered ? (
+
+ ) : (
+
+ )}
+ >
+ )
+};
+
+
+export default Work;
\ No newline at end of file
diff --git a/jb-ui/src/pages/max-and-lulu.jpg b/jb-ui/src/pages/max-and-lulu.jpg
new file mode 100644
index 0000000..e7e0b7d
Binary files /dev/null and b/jb-ui/src/pages/max-and-lulu.jpg differ
--
cgit v1.2.3