diff options
| author | Max Nanis | 2026-02-18 20:42:03 -0500 |
|---|---|---|
| committer | Max Nanis | 2026-02-18 20:42:03 -0500 |
| commit | 3eaa56f0306ead818f64c3d99fc6d230d9b970a4 (patch) | |
| tree | 9fecc2f1456e6321572e0e65f57106916df173e2 /jb-ui/src/pages/Preview.tsx | |
| download | amt-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.tsx | 288 |
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 |
