summaryrefslogtreecommitdiff
path: root/jb-ui/src/pages/Preview.tsx
diff options
context:
space:
mode:
authorMax Nanis2026-02-18 20:42:03 -0500
committerMax Nanis2026-02-18 20:42:03 -0500
commit3eaa56f0306ead818f64c3d99fc6d230d9b970a4 (patch)
tree9fecc2f1456e6321572e0e65f57106916df173e2 /jb-ui/src/pages/Preview.tsx
downloadamt-jb-3eaa56f0306ead818f64c3d99fc6d230d9b970a4.tar.gz
amt-jb-3eaa56f0306ead818f64c3d99fc6d230d9b970a4.zip
HERE WE GO, HERE WE GO, HERE WE GO
Diffstat (limited to 'jb-ui/src/pages/Preview.tsx')
-rw-r--r--jb-ui/src/pages/Preview.tsx288
1 files changed, 288 insertions, 0 deletions
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 (
+ <div className="inline-flex rounded shadow-sm" role="group">
+ {buttons.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 === buttons.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
+ );
+
+ 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}`;
+ }
+
+ 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 week.
+ </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="
+ 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 week.
+ </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="
+ 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>
+ );
+};
+
+
+
+// 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>
+ );
+};
+export default Preview; \ No newline at end of file