aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'jb-ui/src/pages')
-rw-r--r--jb-ui/src/pages/Home.tsx12
-rw-r--r--jb-ui/src/pages/LinkAmtAccount.tsx131
-rw-r--r--jb-ui/src/pages/MagicLink.tsx72
-rw-r--r--jb-ui/src/pages/Preview.tsx523
-rw-r--r--jb-ui/src/pages/Result.tsx206
-rw-r--r--jb-ui/src/pages/Transfer.tsx50
-rw-r--r--jb-ui/src/pages/Work.tsx1452
7 files changed, 1304 insertions, 1142 deletions
diff --git a/jb-ui/src/pages/Home.tsx b/jb-ui/src/pages/Home.tsx
index 5a5c953..8d509b6 100644
--- a/jb-ui/src/pages/Home.tsx
+++ b/jb-ui/src/pages/Home.tsx
@@ -1,13 +1,7 @@
-import max_lulu from "./max-and-lulu.jpg"
+import max_lulu from "./max-and-lulu.jpg";
const Home = function () {
- return (
- <img
- src={max_lulu}
- alt="Max and Lulu"
- className="object-contain"
- />
- )
+ return <img src={max_lulu} alt="Max and Lulu" className="object-contain" />;
};
-export default Home; \ No newline at end of file
+export default Home;
diff --git a/jb-ui/src/pages/LinkAmtAccount.tsx b/jb-ui/src/pages/LinkAmtAccount.tsx
index 0c9dab3..a72626f 100644
--- a/jb-ui/src/pages/LinkAmtAccount.tsx
+++ b/jb-ui/src/pages/LinkAmtAccount.tsx
@@ -5,69 +5,80 @@ import { Button } from "@/components/ui/button";
type RequestState = "idle" | "working" | "sent" | "failed" | "missing-worker";
const LinkAmtAccount = function () {
- const [email, setEmail] = useState("");
- const [state, setState] = useState<RequestState>("idle");
- const [mockLink, setMockLink] = useState<string | null>(null);
- const amtWorkerId = new URLSearchParams(window.location.search).get("workerId");
+ const [email, setEmail] = useState("");
+ const [state, setState] = useState<RequestState>("idle");
+ const [mockLink, setMockLink] = useState<string | null>(null);
+ const amtWorkerId = new URLSearchParams(window.location.search).get(
+ "workerId",
+ );
- const submit = async (event: FormEvent<HTMLFormElement>) => {
- event.preventDefault();
- if (!amtWorkerId) {
- setState("missing-worker");
- return;
- }
+ const submit = async (event: FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ if (!amtWorkerId) {
+ setState("missing-worker");
+ return;
+ }
- setState("working");
- setMockLink(null);
- try {
- const response = await fetch("/auth/link-amt/request", {
- method: "POST",
- credentials: "include",
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify({
- "email": email,
- "amt_worker_id": amtWorkerId,
- }),
- });
- if (!response.ok) throw new Error("Account-link request failed");
- const body = await response.json() as {magic_link?: string};
- setMockLink(body.magic_link ?? null);
- setState("sent");
- } catch {
- setState("failed");
- }
- };
+ setState("working");
+ setMockLink(null);
+ try {
+ const response = await fetch("/auth/link-amt/request", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ email: email,
+ amt_worker_id: amtWorkerId,
+ }),
+ });
+ if (!response.ok) throw new Error("Account-link request failed");
+ const body = (await response.json()) as { magic_link?: string };
+ setMockLink(body.magic_link ?? null);
+ setState("sent");
+ } catch {
+ setState("failed");
+ }
+ };
- return (
- <main className="flex min-h-screen items-center justify-center bg-zinc-200 p-4">
- <section className="w-full max-w-md space-y-4 rounded-lg bg-white p-6 shadow-lg">
- <h1 className="text-xl font-semibold">Link your MTurk account</h1>
- <p>Enter the email address you want to use for James Billings.</p>
- <form className="space-y-4" onSubmit={submit}>
- <input
- className="w-full rounded-md border px-3 py-2"
- type="email"
- autoComplete="email"
- required
- value={email}
- onChange={(event) => setEmail(event.target.value)}
- placeholder="you@example.com"
- />
- <Button type="submit" disabled={state === "working"}>
- {state === "working" ? "Sending…" : "Send link"}
- </Button>
- </form>
- {state === "sent" && <p>Check your email to finish linking your account.</p>}
- {state === "failed" && <p>Unable to send the link. Please try again.</p>}
- {state === "missing-worker" && <p>This page is missing your MTurk worker ID.</p>}
- {mockLink && (
- <p>
- Development link: <a className="underline" href={mockLink}>continue</a>
- </p>
- )}
- </section>
- </main>
- );
+ return (
+ <main className="flex min-h-screen items-center justify-center bg-zinc-200 p-4">
+ <section className="w-full max-w-md space-y-4 rounded-lg bg-white p-6 shadow-lg">
+ <h1 className="text-xl font-semibold">Link your MTurk account</h1>
+ <p>Enter the email address you want to use for James Billings.</p>
+ <form className="space-y-4" onSubmit={submit}>
+ <input
+ className="w-full rounded-md border px-3 py-2"
+ type="email"
+ autoComplete="email"
+ required
+ value={email}
+ onChange={(event) => setEmail(event.target.value)}
+ placeholder="you@example.com"
+ />
+ <Button type="submit" disabled={state === "working"}>
+ {state === "working" ? "Sending…" : "Send link"}
+ </Button>
+ </form>
+ {state === "sent" && (
+ <p>Check your email to finish linking your account.</p>
+ )}
+ {state === "failed" && (
+ <p>Unable to send the link. Please try again.</p>
+ )}
+ {state === "missing-worker" && (
+ <p>This page is missing your MTurk worker ID.</p>
+ )}
+ {mockLink && (
+ <p>
+ Development link:{" "}
+ <a className="underline" href={mockLink}>
+ continue
+ </a>
+ </p>
+ )}
+ </section>
+ </main>
+ );
};
export default LinkAmtAccount;
diff --git a/jb-ui/src/pages/MagicLink.tsx b/jb-ui/src/pages/MagicLink.tsx
index 4042624..eb272a6 100644
--- a/jb-ui/src/pages/MagicLink.tsx
+++ b/jb-ui/src/pages/MagicLink.tsx
@@ -3,42 +3,44 @@ import { useEffect, useRef, useState } from "react";
type ExchangeState = "working" | "failed" | "missing";
const MagicLink = function () {
- const started = useRef(false);
- const [state, setState] = useState<ExchangeState>("working");
-
- useEffect(() => {
- // React Strict Mode runs effects twice in development. Never redeem twice.
- if (started.current) return;
- started.current = true;
-
- const params = new URLSearchParams(window.location.search);
- const token = params.get("token");
- if (!token) {
- setState("missing");
- return;
- }
-
- // Keep the credential out of browser history and subsequent Referer headers.
- window.history.replaceState({}, "", window.location.pathname);
-
- void fetch("/auth/magic-link/exchange", {
- method: "POST",
- credentials: "include",
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify({"token": token}),
- }).then((response) => {
- if (!response.ok) throw new Error("Magic-link exchange failed");
- window.location.replace("/");
- }).catch(() => setState("failed"));
- }, []);
-
- if (state === "missing") {
- return <p>This login link is missing its token.</p>;
+ const started = useRef(false);
+ const [state, setState] = useState<ExchangeState>("working");
+
+ useEffect(() => {
+ // React Strict Mode runs effects twice in development. Never redeem twice.
+ if (started.current) return;
+ started.current = true;
+
+ const params = new URLSearchParams(window.location.search);
+ const token = params.get("token");
+ if (!token) {
+ setState("missing");
+ return;
}
- if (state === "failed") {
- return <p>This login link is invalid or has expired.</p>;
- }
- return <p>Signing you in…</p>;
+
+ // Keep the credential out of browser history and subsequent Referer headers.
+ window.history.replaceState({}, "", window.location.pathname);
+
+ void fetch("/auth/magic-link/exchange", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token: token }),
+ })
+ .then((response) => {
+ if (!response.ok) throw new Error("Magic-link exchange failed");
+ window.location.replace("/");
+ })
+ .catch(() => setState("failed"));
+ }, []);
+
+ if (state === "missing") {
+ return <p>This login link is missing its token.</p>;
+ }
+ if (state === "failed") {
+ return <p>This login link is invalid or has expired.</p>;
+ }
+ return <p>Signing you in…</p>;
};
export default MagicLink;
diff --git a/jb-ui/src/pages/Preview.tsx b/jb-ui/src/pages/Preview.tsx
index 153b4bb..6e50b0d 100644
--- a/jb-ui/src/pages/Preview.tsx
+++ b/jb-ui/src/pages/Preview.tsx
@@ -1,287 +1,300 @@
import {
- Leaderboard,
- LeaderboardApi,
- LeaderboardCode,
- LeaderboardFrequency,
- LeaderboardRow,
+ Leaderboard,
+ LeaderboardApi,
+ LeaderboardCode,
+ LeaderboardFrequency,
+ LeaderboardRow,
} from "@/api_fsb";
import { useAppSelector } from "@/hooks";
-import { bpid, formatCentsToUSD, truncate, } from "@/lib/utils";
+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';
+import moment from "moment";
+import { useEffect, useState } from "react";
const showSmartRank = (
- item: LeaderboardRow,
- index: number,
- items: LeaderboardRow[] | undefined
+ 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;
- }
+ /**
+ * 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;
+ }
+ return true;
};
interface FrequencyButtonsProps {
- selected: LeaderboardFrequency;
- onChange: (frequency: LeaderboardFrequency) => void;
+ selected: LeaderboardFrequency;
+ onChange: (frequency: LeaderboardFrequency) => void;
}
const frequency_map = [
- { label: 'Day', value: LeaderboardFrequency.Daily },
- { label: 'Week', value: LeaderboardFrequency.Weekly },
- { label: 'Month', value: LeaderboardFrequency.Monthly },
+ { label: "Day", value: LeaderboardFrequency.Daily },
+ { label: "Week", value: LeaderboardFrequency.Weekly },
+ { label: "Month", value: LeaderboardFrequency.Monthly },
];
const ThreeButtonToggle = ({ selected, onChange }: FrequencyButtonsProps) => {
- return (
- <div className="inline-flex rounded shadow-sm" role="group">
- {frequency_map.map((button, idx) => (
- <button
- type="button"
- key={button.value}
- onClick={() => 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 === frequency_map.length - 1 && "rounded-r",
- selected === button.value
- ? 'text-zinc-50 bg-zinc-400'
- : 'text-zinc-950 bg-zinc-50',
- )}>
- {button.label}
- </button>
- ))}
- </div>
- );
+ return (
+ <div className="inline-flex rounded shadow-sm" role="group">
+ {frequency_map.map((button, idx) => (
+ <button
+ type="button"
+ key={button.value}
+ onClick={() => 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 === frequency_map.length - 1 && "rounded-r",
+ selected === button.value
+ ? "text-zinc-50 bg-zinc-400"
+ : "text-zinc-950 bg-zinc-50",
+ )}
+ >
+ {button.label}
+ </button>
+ ))}
+ </div>
+ );
};
-
-
// 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>(
- LeaderboardFrequency.Daily
+ /**
+ * 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>(
+ LeaderboardFrequency.Daily,
+ );
+
+ const [payouts, setPayouts] = useState<Leaderboard | null>(null);
+ const [completes, setCompletes] = useState<Leaderboard | null>(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 [payouts, setPayouts] = useState<Leaderboard | null>(null);
- const [completes, setCompletes] = useState<Leaderboard | null>(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}`;
- }
-
- const freq_label = frequency_map.find(f => f.value === frequency)?.label.toLocaleLowerCase()
-
- return (
- <div className="my-2 mx-4 py-0">
- <div className="w-full flex items-center justify-between my-1">
- {/* Left spacer (hidden on small screens) */}
- <div className="hidden md:block md:w-1/3"></div>
-
- {/* Centered text */}
- <h3 className="text-center flex-1 md:1/3">
- Leaderboards
- </h3>
-
- {/* Right-aligned button group */}
- <div className="flex md:w-1/3 justify-end">
- <ThreeButtonToggle
- selected={frequency}
- onChange={setFrequency}
- />
- </div>
- </div>
-
- <div className="grid grid-cols-1 mb-2 text-zinc-700">
- <p className="text-center text-[10px] font-thin">
- {dateRange}
- </p>
- </div>
-
- <div className="grid grid-cols-1 gap-4 px-12 md:px-0
- md:grid-cols-2">
- <table className="border-collapse text-emerald-950 text-[8px] md:text-sm">
- <caption className="caption-top text-xs font-semibold">
- Most earned in the past {freq_label}.
- </caption>
- <thead>
- <tr className="
+ const end = moment(completes?.end_timestamp * 1000).format(
+ "MMM Do, h:mm:ssa",
+ );
+ //dateRange = `${start} – ${end} (${completes?.timezone_name})`;
+ dateRange = `${start} – ${end}`;
+ }
+
+ const freq_label = frequency_map
+ .find((f) => f.value === frequency)
+ ?.label.toLocaleLowerCase();
+
+ return (
+ <div className="my-2 mx-4 py-0">
+ <div className="w-full flex items-center justify-between my-1">
+ {/* Left spacer (hidden on small screens) */}
+ <div className="hidden md:block md:w-1/3"></div>
+
+ {/* Centered text */}
+ <h3 className="text-center flex-1 md:1/3">Leaderboards</h3>
+
+ {/* Right-aligned button group */}
+ <div className="flex md:w-1/3 justify-end">
+ <ThreeButtonToggle selected={frequency} onChange={setFrequency} />
+ </div>
+ </div>
+
+ <div className="grid grid-cols-1 mb-2 text-zinc-700">
+ <p className="text-center text-[10px] font-thin">{dateRange}</p>
+ </div>
+
+ <div
+ className="grid grid-cols-1 gap-4 px-12 md:px-0
+ md:grid-cols-2"
+ >
+ <table className="border-collapse text-emerald-950 text-[8px] md:text-sm">
+ <caption className="caption-top text-xs font-semibold">
+ Most earned in the past {freq_label}.
+ </caption>
+ <thead>
+ <tr
+ className="
font-mono font-semibold text-center
- border border-zinc-150">
- <th>Rank</th>
- <th>Bonus</th>
- <th>User</th>
- </tr>
- </thead>
- <tbody className="font-mono">
-
- {payouts?.rows?.map((dataItem: LeaderboardRow) => (
- <tr key={dataItem.bpuid} className="
+ border border-zinc-150"
+ >
+ <th>Rank</th>
+ <th>Bonus</th>
+ <th>User</th>
+ </tr>
+ </thead>
+ <tbody className="font-mono">
+ {payouts?.rows?.map((dataItem: LeaderboardRow) => (
+ <tr
+ key={dataItem.bpuid}
+ className="
text-center
- border border-zinc-100">
- <td className="font-medium">
- {dataItem.rank}
- </td>
- <td className="font-semibold">
- {formatCentsToUSD(dataItem.value)}
- </td>
- <td className="font-light">
- {truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
- </td>
- </tr>
- ))}
- </tbody>
- </table>
-
- <table className="border-collapse border border-gray-400
- text-emerald-950 text-[8px] md:text-sm">
- <caption className="caption-top text-xs font-semibold">
- Top completes in the past {freq_label}.
- </caption>
- <thead>
- <tr className="
+ border border-zinc-100"
+ >
+ <td className="font-medium">{dataItem.rank}</td>
+ <td className="font-semibold">
+ {formatCentsToUSD(dataItem.value)}
+ </td>
+ <td className="font-light">
+ {truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+
+ <table
+ className="border-collapse border border-gray-400
+ text-emerald-950 text-[8px] md:text-sm"
+ >
+ <caption className="caption-top text-xs font-semibold">
+ Top completes in the past {freq_label}.
+ </caption>
+ <thead>
+ <tr
+ className="
font-mono font-semibold text-center
- border border-zinc-150 rounded">
- <th>Rank</th>
- <th>Completes</th>
- <th>User</th>
- </tr>
- </thead>
- <tbody className="font-mono">
-
- {completes?.rows?.map((dataItem: LeaderboardRow, index: number) => (
- <tr key={dataItem.bpuid} className="
+ border border-zinc-150 rounded"
+ >
+ <th>Rank</th>
+ <th>Completes</th>
+ <th>User</th>
+ </tr>
+ </thead>
+ <tbody className="font-mono">
+ {completes?.rows?.map((dataItem: LeaderboardRow, index: number) => (
+ <tr
+ key={dataItem.bpuid}
+ className="
text-center
- border border-zinc-100">
- <td className="font-medium">
- {dataItem.rank ?? showSmartRank(dataItem, index, completes?.rows)}
- </td>
- <td className="font-semibold">
- {dataItem.value}
- </td>
- <td className="font-light">
- {truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- );
+ border border-zinc-100"
+ >
+ <td className="font-medium">
+ {dataItem.rank ??
+ showSmartRank(dataItem, index, completes?.rows)}
+ </td>
+ <td className="font-semibold">{dataItem.value}</td>
+ <td className="font-light">
+ {truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ );
};
-
-
// 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 (
- <div className="max-h-[calc(100vh-10rem)] overflow-y-auto">
-
- <div className="bg-zinc-100 rounded py-2 m-0">
- <p className="text-center text-sm italic text-zinc-600">
- {users_f} people are actively attempting {surveys_f} available surveys right now.
- </p>
- </div>
-
- <Leaderboards />
-
- <div className="bg-zinc-100 rounded py-2 px-6 m-0 text-xs text-center">
- <p className="py-1">
- 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.</p>
- <p className="py-1">
- 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.</p>
- </div>
- </div>
- );
+ /**
+ *
+ * @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 (
+ <div className="max-h-[calc(100vh-10rem)] overflow-y-auto">
+ <div className="bg-zinc-100 rounded py-2 m-0">
+ <p className="text-center text-sm italic text-zinc-600">
+ {users_f} people are actively attempting {surveys_f} available surveys
+ right now.
+ </p>
+ </div>
+
+ <Leaderboards />
+
+ <div className="bg-zinc-100 rounded py-2 px-6 m-0 text-xs text-center">
+ <p className="py-1">
+ 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.
+ </p>
+ <p className="py-1">
+ 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.
+ </p>
+ </div>
+ </div>
+ );
};
-export default Preview; \ No newline at end of file
+export default Preview;
diff --git a/jb-ui/src/pages/Result.tsx b/jb-ui/src/pages/Result.tsx
index 47af248..477f77e 100644
--- a/jb-ui/src/pages/Result.tsx
+++ b/jb-ui/src/pages/Result.tsx
@@ -10,114 +10,124 @@ import { useEffect } from "react";
// }
export const Complete = () => {
- const ts = useAppSelector(state => state.app.taskStatus)
-
- return (
- <>
- <h2 className="text-lg">Survey Completed</h2>
- <p className="text-emerald-700 text-lg font-bold">Congratulations! You completed the Survey.</p>
- <p className="text-emerald-700 text-2xl font-bold">{ts?.user_payout_string}</p>
- <p className="text-slate-300">You may close this tab and return to submit the HIT</p>
- </>
- )
-}
+ const ts = useAppSelector((state) => state.app.taskStatus);
+
+ return (
+ <>
+ <h2 className="text-lg">Survey Completed</h2>
+ <p className="text-emerald-700 text-lg font-bold">
+ Congratulations! You completed the Survey.
+ </p>
+ <p className="text-emerald-700 text-2xl font-bold">
+ {ts?.user_payout_string}
+ </p>
+ <p className="text-slate-300">
+ You may close this tab and return to submit the HIT
+ </p>
+ </>
+ );
+};
export const Enter = () => {
- return (
- <>
- <h2 className="text-lg">Entered</h2>
- <p className="text-slate-800 text-lg font-bold">You're currently in the Survey</p>
- <p className="text-slate-300">You may close this tab and return to submit the HIT</p>
- </>
- )
-}
+ return (
+ <>
+ <h2 className="text-lg">Entered</h2>
+ <p className="text-slate-800 text-lg font-bold">
+ You're currently in the Survey
+ </p>
+ <p className="text-slate-300">
+ You may close this tab and return to submit the HIT
+ </p>
+ </>
+ );
+};
export const InComplete = () => {
- const ts = useAppSelector(state => state.app.taskStatus)
- if (!ts) { return null; }
-
- return (
- <>
- <h2 className="text-lg">HIT Complete</h2>
- <p className="text-rose-800 text-lg font-bold">Survey terminated the attempt.</p>
- <p className="text-slate-300">You may close this tab and return to submit the HIT</p>
- </>
-
- )
-}
+ const ts = useAppSelector((state) => state.app.taskStatus);
+ if (!ts) {
+ return null;
+ }
+
+ return (
+ <>
+ <h2 className="text-lg">HIT Complete</h2>
+ <p className="text-rose-800 text-lg font-bold">
+ Survey terminated the attempt.
+ </p>
+ <p className="text-slate-300">
+ You may close this tab and return to submit the HIT
+ </p>
+ </>
+ );
+};
export const Unknown = () => {
-
- return (
- <>
- <h2 className="text-lg">Task Issue</h2>
- <p className="text-rose-800 text-lg font-bold">You're not currently eligible for any Surveys</p>
- <p className="text-slate-300">You may close this tab and return to submit the HIT</p>
- </>
- )
-}
+ return (
+ <>
+ <h2 className="text-lg">Task Issue</h2>
+ <p className="text-rose-800 text-lg font-bold">
+ You're not currently eligible for any Surveys
+ </p>
+ <p className="text-slate-300">
+ You may close this tab and return to submit the HIT
+ </p>
+ </>
+ );
+};
export const Loading = () => {
-
- return (
- <p className="text-2xl font-semibold">Loading...</p>
- )
-}
-
+ return <p className="text-2xl font-semibold">Loading...</p>;
+};
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()
+ 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();
}
-
- useEffect(() => {
- if (ts?.status === 3) {
- confettiCannons()
- }
- }, [ts])
-
-
- switch (ts?.status) {
- case 0: // UNKNOWN
- return <Unknown />;
- case 1: // ENTER
- return <Enter />;
- case 2: // INCOMPLETE
- return <InComplete />;
- case 3: // COMPLETE
- return <Complete />;
- default:
- return <Loading />;
- }
-
+ }, [ts]);
+
+ switch (ts?.status) {
+ case 0: // UNKNOWN
+ return <Unknown />;
+ case 1: // ENTER
+ return <Enter />;
+ case 2: // INCOMPLETE
+ return <InComplete />;
+ case 3: // COMPLETE
+ return <Complete />;
+ default:
+ return <Loading />;
+ }
};
-
-export default Result; \ No newline at end of file
+export default Result;
diff --git a/jb-ui/src/pages/Transfer.tsx b/jb-ui/src/pages/Transfer.tsx
new file mode 100644
index 0000000..6e087d6
--- /dev/null
+++ b/jb-ui/src/pages/Transfer.tsx
@@ -0,0 +1,50 @@
+import { clsx } from "clsx";
+
+import { useSelector } from "react-redux";
+
+const Transfer = function () {
+ let error = [];
+
+ const handleInputChange = (
+ event: React.KeyboardEvent<HTMLInputElement>,
+ ) => {};
+
+ const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {};
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ console.log("Form submitted successfully!");
+ };
+
+ return (
+ <div className="px-6 my-6">
+ <h2
+ className="
+ bg-red-500 rounded py-2
+ text-white text-2xl font-semibold text-center"
+ >
+ Transfer Required
+ </h2>
+ <p
+ className="
+ py-4
+ text-zinc-500 text-2xl font-semibold text-center"
+ >
+ James Billings is finished 😭 Just Kidding!!!
+ </p>
+
+ <input
+ type="text"
+ id="text-entry-input"
+ aria-describedby=""
+ // defaultValue={answer?.values.length ? answer?.values[0] : ""}
+ // onKeyUp={handleInputChange}
+ // onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press
+ // title={error ? answer?.error_msg : ""}
+ className={clsx("w-full border-1 rounded px-2 py-1")}
+ />
+ </div>
+ );
+};
+
+export default Transfer;
diff --git a/jb-ui/src/pages/Work.tsx b/jb-ui/src/pages/Work.tsx
index c54732f..32dcc96 100644
--- a/jb-ui/src/pages/Work.tsx
+++ b/jb-ui/src/pages/Work.tsx
@@ -1,765 +1,847 @@
import {
- BucketTask,
- OfferwallApi,
- OfferwallReason,
- ReportApi, ReportTask, ReportValue, StatusApi,
- TasksStatusResponse, TaskStatusResponse, TopNPlusBlockOfferWall
+ 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,
+ 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
+ getAvailabilityCount,
+ getLOIText,
+ isLowBalance,
+ selectBucket,
+ setAttemptedLiveEligibleCount,
+ setAvailabilityCount,
+ setCurrentBuckets,
+ setEnteredTimestamp,
+ setLOI,
+ setOfferwallReasons,
+ setTaskStatus,
} from "@/models/appSlice";
import {
- createColumnHelper, flexRender, getCoreRowModel,
- useReactTable
+ 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';
+import Markdown from "react-markdown";
const Report = ({ onClose }: { onClose: () => void }) => {
- const [showComplete, setShowComplete] = useState(false);
- const [selectedValues, setSelectedValues] = useState<ReportValue[]>([]);
-
- 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 [showComplete, setShowComplete] = useState(false);
+ const [selectedValues, setSelectedValues] = useState<ReportValue[]>([]);
+
+ 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: "",
};
- 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 <p>Please finish a Task to report its status.</p>
- }
-
- if (showComplete) {
- return <SubimtAMT />
- }
-
- return (
- <div className="border-2 border-rose-200 rounded">
- <div className="flex justify-center">
- <FieldSet className="mt-2">
- <FieldLegend variant="label" className="my-0 text-rose-950">
- Please select all reasons why you are reporting this Task:
- </FieldLegend>
- <FieldDescription className="my-0 text-rose-950">
- You must provide at least one reason to report a Task.
- </FieldDescription>
- <FieldGroup className="gap-2 my-0 py-0">
-
- {reportReasons.map((reason) => (
- <Field key={reason.id} orientation="horizontal" className="
- hover:cursor-pointer">
- <Checkbox
- id={reason.id}
- name={reason.id}
- onCheckedChange={(checked) =>
- handleCheckboxChange(reason.value, checked as boolean)
- }
- />
- <FieldLabel
- htmlFor={reason.id}
- className="font-medium text-rose-950
+ 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 <p>Please finish a Task to report its status.</p>;
+ }
+
+ if (showComplete) {
+ return <SubimtAMT />;
+ }
+
+ return (
+ <div className="border-2 border-rose-200 rounded">
+ <div className="flex justify-center">
+ <FieldSet className="mt-2">
+ <FieldLegend variant="label" className="my-0 text-rose-950">
+ Please select all reasons why you are reporting this Task:
+ </FieldLegend>
+ <FieldDescription className="my-0 text-rose-950">
+ You must provide at least one reason to report a Task.
+ </FieldDescription>
+ <FieldGroup className="gap-2 my-0 py-0">
+ {reportReasons.map((reason) => (
+ <Field
+ key={reason.id}
+ orientation="horizontal"
+ className="
+ hover:cursor-pointer"
+ >
+ <Checkbox
+ id={reason.id}
+ name={reason.id}
+ onCheckedChange={(checked) =>
+ handleCheckboxChange(reason.value, checked as boolean)
+ }
+ />
+ <FieldLabel
+ htmlFor={reason.id}
+ className="font-medium text-rose-950
hover:cursor-pointer hover:font-semibold"
- >
- {reason.label}
- </FieldLabel>
- </Field>
- ))}
-
- </FieldGroup>
- </FieldSet>
- </div>
- <div className="flex justify-center my-2 space-x-5">
- <Button variant="outline" size="icon" aria-label="Submit"
- className="
+ >
+ {reason.label}
+ </FieldLabel>
+ </Field>
+ ))}
+ </FieldGroup>
+ </FieldSet>
+ </div>
+ <div className="flex justify-center my-2 space-x-5">
+ <Button
+ variant="outline"
+ size="icon"
+ aria-label="Submit"
+ className="
border-2 rounded border-zinc-800 text-zinc-800
hover:cursor-pointer hover:bg-zinc-800 hover:text-zinc-50"
- onClick={onClose}>
- <XIcon />
- </Button>
- <Button variant="outline"
- className="
+ onClick={onClose}
+ >
+ <XIcon />
+ </Button>
+ <Button
+ variant="outline"
+ className="
border-2 rounded border-rose-400 text-rose-950
hover:cursor-pointer hover:bg-rose-400 hover:text-rose-50"
- onClick={clicked_submit}>Submit and Complete HIT</Button>
- </div>
- </div>
- )
-}
+ onClick={clicked_submit}
+ >
+ Submit and Complete HIT
+ </Button>
+ </div>
+ </div>
+ );
+};
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 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<HTMLFormElement>(null);
+ const formRef = useRef<HTMLFormElement>(null);
- useEffect(() => {
- if (!ts) return;
- if (!bpuid) return;
- if (!assignment_id) return;
- if (!turkSubmitTo) return;
+ 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]);
+ if (formRef.current) {
+ formRef.current.submit();
+ }
+ }, [ts?.tsid, bpuid, assignment_id, turkSubmitTo]);
+
+ return (
+ <>
+ <form
+ ref={formRef}
+ action={`${turkSubmitTo}/mturk/externalSubmit`}
+ method="POST"
+ style={{ display: "none" }}
+ >
+ <input type="hidden" name="assignmentId" value={assignment_id} />
+ <input type="hidden" name="tsid" value={ts?.tsid} />
+ <input type="hidden" name="amt_assignment_id" value={assignment_id} />
+ <input type="hidden" name="amt_worker_id" value={bpuid} />
+ <button type="submit">Submit HIT</button>
+ </form>
+ </>
+ );
+};
- return (
- <>
- <form
- ref={formRef}
- action={`${turkSubmitTo}/mturk/externalSubmit`}
- method="POST"
- style={{ display: 'none' }}>
- <input type="hidden" name="assignmentId" value={assignment_id} />
- <input type="hidden" name="tsid" value={ts?.tsid} />
- <input type="hidden" name="amt_assignment_id" value={assignment_id} />
- <input type="hidden" name="amt_worker_id" value={bpuid} />
- <button type="submit">Submit HIT</button>
- </form>
- </>
- )
-}
+const SurveyMonitor = () => {
+ const dispatch = useAppDispatch();
+ const [isPolling, setIsPolling] = useState(true);
+ // const [timeAgo, setTimeAgo] = useState("");
-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 (
- <p>Loading</p>
- )
- }
+ const [showComplete, setShowComplete] = useState(false);
+ const [showReport, setShowReport] = useState(false);
- // 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 (
- <>
- <div>
- <h2 className="
- bg-blue-500 rounded py-2
- text-white text-2xl font-semibold text-center">
- In Progress
- </h2>
- <p className="
- py-4
- text-zinc-500 text-2xl font-semibold text-center">
- We're actively monitoring your Task status.
- </p>
- </div>
- </>
+ 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);
+ // }
+ };
- // 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
+ // 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 <p>Loading</p>;
+ }
+
+ // 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 (
- <>
- <div>
- <h2 className="
+ <>
+ <div>
+ <h2
+ className="
bg-blue-500 rounded py-2
- text-white text-2xl font-semibold text-center">
- {ts.status === 3 ? "Survey Complete!" : "Survey Terminated."}
- </h2>
-
- {((ts.status === 3 || !isLow) && !isBaddie) && (
- <p className="
+ text-white text-2xl font-semibold text-center"
+ >
+ In Progress
+ </h2>
+ <p
+ className="
+ py-4
+ text-zinc-500 text-2xl font-semibold text-center"
+ >
+ We're actively monitoring your Task status.
+ </p>
+ </div>
+ </>
+ );
+ }
+
+ // 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 (
+ <>
+ <div>
+ <h2
+ className="
+ bg-blue-500 rounded py-2
+ text-white text-2xl font-semibold text-center"
+ >
+ {ts.status === 3 ? "Survey Complete!" : "Survey Terminated."}
+ </h2>
+
+ {(ts.status === 3 || !isLow) && !isBaddie && (
+ <p
+ className="
py-2
- text-zinc-500 text-md font-medium text-center">
- You may now submit the HIT.
- </p>
- )}
- </div>
-
- {(isBaddie) ? (
- <>
- <div className="
+ text-zinc-500 text-md font-medium text-center"
+ >
+ You may now submit the HIT.
+ </p>
+ )}
+ </div>
+
+ {isBaddie ? (
+ <>
+ <div
+ className="
border-2 border-rose-500 rounded py-2
- bg-rose-400 text-rose-700">
- <h2 className="text-center text-xl font-bold text-white">
- Quality Check Failed.
- </h2>
- <p className="px-4 py-2 text-center text-sm font-medium text-white">
- Unfortunately, it looks like you did not pass the quality check for this survey.
- </p>
-
- <p className="
+ bg-rose-400 text-rose-700"
+ >
+ <h2 className="text-center text-xl font-bold text-white">
+ Quality Check Failed.
+ </h2>
+ <p className="px-4 py-2 text-center text-sm font-medium text-white">
+ Unfortunately, it looks like you did not pass the quality check
+ for this survey.
+ </p>
+
+ <p
+ className="
pt-0 pb-4
- text-red-950 text-sm font-semibold text-center">
- Please return the HIT to avoid having your assignment rejected.
- </p>
- </div>
- </>
-
- ) : (
- <>
- {(ts.status === 3 || !isLow) ? (
- <>
- <div className="flex justify-center my-4 space-x-5">
-
- <button type="button" className="
+ text-red-950 text-sm font-semibold text-center"
+ >
+ Please return the HIT to avoid having your assignment rejected.
+ </p>
+ </div>
+ </>
+ ) : (
+ <>
+ {ts.status === 3 || !isLow ? (
+ <>
+ <div className="flex justify-center my-4 space-x-5">
+ <button
+ type="button"
+ className="
border-4 rounded border-emerald-400 py-1 px-3
text-xl font-bold text-emerald-950
hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50"
- onClick={() => setShowComplete(true)}
- >Submit HIT</button>
-
- <button type="button" className="
+ onClick={() => setShowComplete(true)}
+ >
+ Submit HIT
+ </button>
+
+ <button
+ type="button"
+ className="
border-4 rounded border-rose-200 py-1 px-3
text-xl font-bold text-rose-200
hover:cursor-pointer hover:border-rose-500 hover:bg-rose-400 hover:text-rose-50"
- onClick={() => setShowReport(!showReport)}
- >Report Survey</button>
- </div>
-
- {showComplete && <SubimtAMT />}
- {showReport && <Report onClose={() => setShowReport(false)} />}
- </>
- ) : (
- <div>
- <p className="
- text-red-600 text-lg font-semibold text-center">
- Your balance is low. You cannot submit the HIT at this time.
- </p>
-
- <p className="
- pt-0 pb-4
- text-red-950 text-sm font-semibold text-center">
- Please return the HIT to avoid having your assignment rejected.
- </p>
- </div>
- )}
- </>
-
- )}
-
+ onClick={() => setShowReport(!showReport)}
+ >
+ Report Survey
+ </button>
+ </div>
+
+ {showComplete && <SubimtAMT />}
+ {showReport && <Report onClose={() => setShowReport(false)} />}
+ </>
+ ) : (
+ <div>
+ <p
+ className="
+ text-red-600 text-lg font-semibold text-center"
+ >
+ Your balance is low. You cannot submit the HIT at this time.
+ </p>
+ <p
+ className="
+ pt-0 pb-4
+ text-red-950 text-sm font-semibold text-center"
+ >
+ Please return the HIT to avoid having your assignment rejected.
+ </p>
+ </div>
+ )}
</>
- )
-}
-
+ )}
+ </>
+ );
+};
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 (
- <div className="bg-zinc-100 rounded py-2 m-0">
- <p className="text-zinc-700 text-2xl text-center font-bold m-0">
- I have <span
- className="cursor-pointer text-emerald-600 italic hover:text-emerald-700 hover:underline"
- onClick={toggle_loi}>{time_f}</span> minutes
- </p>
- <p
- className="text-center text-xs font-thin m-0 text-zinc-500">
- The average time spent is {avg_time_f} minutes and pays {avg_pay_f}</p>
- </div>
- )
-
+ // 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 (
+ <div className="bg-zinc-100 rounded py-2 m-0">
+ <p className="text-zinc-700 text-2xl text-center font-bold m-0">
+ I have{" "}
+ <span
+ className="cursor-pointer text-emerald-600 italic hover:text-emerald-700 hover:underline"
+ onClick={toggle_loi}
+ >
+ {time_f}
+ </span>{" "}
+ minutes
+ </p>
+ <p className="text-center text-xs font-thin m-0 text-zinc-500">
+ The average time spent is {avg_time_f} minutes and pays {avg_pay_f}
+ </p>
+ </div>
+ );
};
const SurveyLoading = () => {
- return (
- <div className="
+ return (
+ <div
+ className="
border-8 border-emerald-400 rounded py-0 my-2
text-center text-3xl
- hover:cursor-pointer hover:bg-green-500 hover:text-white">
- <p className="py-2 text-center text-3xl font-semibold">
- Survey Loading…
- </p>
- </div>
- )
-}
-
+ hover:cursor-pointer hover:bg-green-500 hover:text-white"
+ >
+ <p className="py-2 text-center text-3xl font-semibold">Survey Loading…</p>
+ </div>
+ );
+};
const NoSurveys = () => {
- const offerwall_reasons = useAppSelector(state => state.app.offerwall_reasons)
-
- const REASON_DISPLAY: Record<OfferwallReason, { label: string; description: string }> = {
- [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]);
- }
+ const offerwall_reasons = useAppSelector(
+ (state) => state.app.offerwall_reasons,
+ );
- return (
- <>
- <div className="
+ const REASON_DISPLAY: Record<
+ OfferwallReason,
+ { label: string; description: string }
+ > = {
+ [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 (
+ <>
+ <div
+ className="
border-8 border-emerald-400 rounded py-0 my-2
text-center text-3xl
- hover:cursor-pointer hover:bg-green-500 hover:text-white">
- <p className="py-2 text-center text-3xl font-semibold">
- No Surveys available…
- </p>
- </div>
-
- {formatReasons(offerwall_reasons).map(r => {
- return (
- <div className="
+ hover:cursor-pointer hover:bg-green-500 hover:text-white"
+ >
+ <p className="py-2 text-center text-3xl font-semibold">
+ No Surveys available…
+ </p>
+ </div>
+
+ {formatReasons(offerwall_reasons).map((r) => {
+ return (
+ <div
+ className="
border-6 border-rose-200 rounded py-0 my-2
text-center
- hover:cursor-pointer hover:bg-rose-200 hover:text-rose-950 ">
- <p className="py-2 text-center text-md font-semibold text-rose-600">
- {r.description}
- </p>
- </div>
- )
- })}
- </>
- )
-}
+ hover:cursor-pointer hover:bg-rose-200 hover:text-rose-950 "
+ >
+ <p className="py-2 text-center text-md font-semibold text-rose-600">
+ {r.description}
+ </p>
+ </div>
+ );
+ })}
+ </>
+ );
+};
const BucketDetails = () => {
- const bucket = useAppSelector(state => selectBucket(state))
- if (!bucket) return null;
- const [showDetails, setShowDetails] = useState(false);
-
- const columnHelper = createColumnHelper<BucketTask>()
- 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 (
- <>
- <div className="w-full text-center text-[8px] text-emerald-600
+ const bucket = useAppSelector((state) => selectBucket(state));
+ if (!bucket) return null;
+ const [showDetails, setShowDetails] = useState(false);
+
+ const columnHelper = createColumnHelper<BucketTask>();
+ 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 (
+ <>
+ <div
+ className="w-full text-center text-[8px] text-emerald-600
rounded py-2 my-1
hover:cursor-pointer hover:text-emerald-700 hover:underline hover:bg-emerald-200
"
- onClick={() => setShowDetails(!showDetails)}
- >
- ({showDetails ? 'Hide' : 'Show'} details of surveys in this bucket)
- </div>
-
- {showDetails && (
- <div className="w-full border-t-1 border-emerald-200 py-1 my-0 flex justify-center">
- <table className="text-[10px] text-emerald-900 table-fixed border-collapse">
- <thead className="font-bold">
- {table.getHeaderGroups().map((headerGroup) => (
-
- <tr key={headerGroup.id}>
- {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 (
- <th
- key={header.id}
- className={`p-0 m-0 ${alignClass}`}
- style={{
- width: `${header.column.getSize()}px`,
- }}
- >
- {flexRender(
- header.column.columnDef.header,
- header.getContext(),
- )}
- </th>
- )
- })}
- </tr>
- ))}
- </thead>
- <tbody>
- {table.getRowModel().rows.map((row) => (
- <tr key={row.id}>
- {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 (
- <td key={cell.id}
- className={`p-0 ${alignClass}`}
- style={{
- width: `${cell.column.getSize()}px`,
- }}
- >
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </td>
- )
- })}
- </tr>
- ))}
- </tbody>
-
- </table>
- </div>
- )}
- </>
- )
+ onClick={() => setShowDetails(!showDetails)}
+ >
+ ({showDetails ? "Hide" : "Show"} details of surveys in this bucket)
+ </div>
+
+ {showDetails && (
+ <div className="w-full border-t-1 border-emerald-200 py-1 my-0 flex justify-center">
+ <table className="text-[10px] text-emerald-900 table-fixed border-collapse">
+ <thead className="font-bold">
+ {table.getHeaderGroups().map((headerGroup) => (
+ <tr key={headerGroup.id}>
+ {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 (
+ <th
+ key={header.id}
+ className={`p-0 m-0 ${alignClass}`}
+ style={{
+ width: `${header.column.getSize()}px`,
+ }}
+ >
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+ </th>
+ );
+ })}
+ </tr>
+ ))}
+ </thead>
+ <tbody>
+ {table.getRowModel().rows.map((row) => (
+ <tr key={row.id}>
+ {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 (
+ <td
+ key={cell.id}
+ className={`p-0 ${alignClass}`}
+ style={{
+ width: `${cell.column.getSize()}px`,
+ }}
+ >
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+ </td>
+ );
+ })}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </>
+ );
};
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 <SurveyLoading />
- }
-
- if (available_cnt === 0 || buckets.length === 0 || !bucket) {
- return <NoSurveys />
- }
-
- return (
- <>
- {isLow && (
- <div className="
+ 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 <SurveyLoading />;
+ }
+
+ if (available_cnt === 0 || buckets.length === 0 || !bucket) {
+ return <NoSurveys />;
+ }
+
+ return (
+ <>
+ {isLow && (
+ <div
+ className="
border-8 border-rose-500 rounded py-2
- bg-rose-400 text-rose-700">
- <h2 className="text-center text-xl font-bold text-white">
- Your balance is low.
- </h2>
- <p className="px-4 py-2 text-center text-sm font-medium text-white">
- 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.
- </p>
- <p className="px-4 py-2 text-center text-sm font-bold text-white">
- HIT submissions with a balance exceeding -$1.00 will
- return in rejected assignments.
- </p>
- </div>
- )}
-
- <LOISelect />
- <div
- onClick={start_survey}
- className="
+ bg-rose-400 text-rose-700"
+ >
+ <h2 className="text-center text-xl font-bold text-white">
+ Your balance is low.
+ </h2>
+ <p className="px-4 py-2 text-center text-sm font-medium text-white">
+ 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.
+ </p>
+ <p className="px-4 py-2 text-center text-sm font-bold text-white">
+ HIT submissions with a balance exceeding -$1.00 will return in
+ rejected assignments.
+ </p>
+ </div>
+ )}
+
+ <LOISelect />
+ <div
+ onClick={start_survey}
+ className="
border-8 border-emerald-400 rounded py-0 my-2
- hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50">
- <p className="py-2 text-center text-3xl font-semibold">
- <a
- target="_blank"
- rel="noopener noreferrer">
- Start Task
- </a>
- </p>
- </div>
- <BucketDetails />
-
- <div className="border-2 border-emerald-200 rounded py-2 m-0">
- <p className="text-emerald-700 text-sm text-center font-medium px-2 m-0">
- <Markdown>
- {bucket.eligibility_explanation}
- </Markdown>
- </p>
- </div>
- </>
- )
+ hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50"
+ >
+ <p className="py-2 text-center text-3xl font-semibold">
+ <a target="_blank" rel="noopener noreferrer">
+ Start Task
+ </a>
+ </p>
+ </div>
+ <BucketDetails />
+
+ <div className="border-2 border-emerald-200 rounded py-2 m-0">
+ <p className="text-emerald-700 text-sm text-center font-medium px-2 m-0">
+ <Markdown>{bucket.eligibility_explanation}</Markdown>
+ </p>
+ </div>
+ </>
+ );
};
-
const Work = () => {
- const entered = useAppSelector(state => state.app.currentBucketEntered)
- const assignment_id = useAppSelector(state => state.app.assignment_id)
-
- if (!assignment_id) {
- return (
- <p className="text-center text-2xl py-3 text-red-800 font-semibold">
- No Assignment ID found. Please access this page through
- the Amazon Mechanical Turk platform.
- </p>
- )
- }
+ const entered = useAppSelector((state) => state.app.currentBucketEntered);
+ const assignment_id = useAppSelector((state) => state.app.assignment_id);
+ if (!assignment_id) {
return (
- <>
- {entered ? (
- <SurveyMonitor />
- ) : (
- <SurveyEnter />
- )}
- </>
- )
+ <p className="text-center text-2xl py-3 text-red-800 font-semibold">
+ No Assignment ID found. Please access this page through the Amazon
+ Mechanical Turk platform.
+ </p>
+ );
+ }
+
+ return <>{entered ? <SurveyMonitor /> : <SurveyEnter />}</>;
};
-
-export default Work; \ No newline at end of file
+export default Work;