aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/pages/Preview.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'jb-ui/src/pages/Preview.tsx')
-rw-r--r--jb-ui/src/pages/Preview.tsx609
1 files changed, 358 insertions, 251 deletions
diff --git a/jb-ui/src/pages/Preview.tsx b/jb-ui/src/pages/Preview.tsx
index 153b4bb..c58a85e 100644
--- a/jb-ui/src/pages/Preview.tsx
+++ b/jb-ui/src/pages/Preview.tsx
@@ -1,287 +1,394 @@
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 { FormEvent, 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 = undefined;
+
+ useEffect(() => {
+ new LeaderboardApi()
+ .timespanLeaderboardProductIdLeaderboardTimespanBoardCodeGet(
+ bpid,
+ LeaderboardCode.CompleteCount,
+ frequency,
+ "us",
+ 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>
+ 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>
+ );
+};
+
+type RequestState = "idle" | "working" | "failed";
+
+interface LoginFormProps {
+ onSubmitSuccess: () => void;
+}
+
+const LoginForm = function ({ onSubmitSuccess }: LoginFormProps) {
+ const [email, setEmail] = useState("");
+ const [state, setState] = useState<RequestState>("idle");
+
+ // const dispatch = useAppDispatch();
+ const submit = async (event: FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ setState("working");
+
+ try {
+ const response = await fetch("/auth/magic-link/request", {
+ method: "POST",
+ credentials: "include",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ email: email,
+ }),
+ });
+
+ if (!response.ok) throw new Error("Account-link request failed");
+ const body = (await response.json()) as { magic_link?: string };
+ console.log(body.magic_link ?? null);
+ onSubmitSuccess();
+ } catch {
+ setState("failed");
+ }
+ };
+
+ return (
+ <>
+ <form className="w-full" onSubmit={submit}>
+ <div className="flex items-center border-b-4 border-blue-700 py-2">
+ <input
+ className="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none font-semibold"
+ type="email"
+ autoComplete="email"
+ required
+ value={email}
+ onChange={(event) => setEmail(event.target.value)}
+ placeholder="hello@jamesbillings67.com"
+ aria-label="Email address"
+ />
+
+ <button
+ className="flex-shrink-0 bg-blue-700 hover:bg-blue-800 text-white py-1 px-2 rounded cursor-pointer font-semibold"
+ type="submit"
+ disabled={state === "working"}
+ >
+ {state === "working" ? "Sending…" : "Login / Signup"}
+ </button>
</div>
- );
+ </form>
+
+ {state === "failed" && <p>Unable to send the link. Please try again.</p>}
+ </>
+ );
};
+const LoginSent = function () {
+ return (
+ <div className="my-1">
+ <h2
+ className="
+ bg-emerald-400 rounded py-2
+ text-white text-l font-semibold text-center"
+ >
+ Please check your email.
+ </h2>
+ <p className="text-slate-800 text-xs font-semibold text-center my-1 py-2">
+ If you don't recieve anything, please check your spam folder, or try
+ again shortly.
+ </p>
+ </div>
+ );
+};
+const Login = () => {
+ const [submitted, setSubmitted] = useState(false);
+
+ return (
+ <>
+ {submitted ? (
+ <LoginSent />
+ ) : (
+ <LoginForm onSubmitSuccess={() => setSubmitted(true)} />
+ )}
+ </>
+ );
+};
// 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>
+ /**
+ *
+ * @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 (
+ <>
+ <Login />
+
+ <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. 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>
+ </div>
+ </div>
+ </>
+ );
};
-export default Preview; \ No newline at end of file
+export default Preview;