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 { FormEvent, 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 frequency_map = [
{ label: "Day", value: LeaderboardFrequency.Daily },
{ label: "Week", value: LeaderboardFrequency.Weekly },
{ label: "Month", value: LeaderboardFrequency.Monthly },
];
const ThreeButtonToggle = ({ selected, onChange }: FrequencyButtonsProps) => {
return (
{frequency_map.map((button, idx) => (
onChange(button.value)}
className={clsx(
"px-2 py-1 text-[10px] font-medium",
"border-t border-b border-gray-200",
"hover:cursor-pointer hover:bg-zinc-200 hover:text-zinc-950",
idx === 0 && "rounded-l",
idx === frequency_map.length - 1 && "rounded-r",
selected === button.value
? "text-zinc-50 bg-zinc-400"
: "text-zinc-950 bg-zinc-50",
)}
>
{button.label}
))}
);
};
// Leaderboards Component
export const Leaderboards = () => {
/**
* Leaderboards do not do any caching, or better state management. This is okay because
* the API is handled to take it. However, it can easily handled better client
* side rather than making the
* **/
const [frequency, setFrequency] = useState(
LeaderboardFrequency.Daily,
);
const [payouts, setPayouts] = useState(null);
const [completes, setCompletes] = useState(null);
const bpuid = 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 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 (
{/* Left spacer (hidden on small screens) */}
{/* Centered text */}
Leaderboards
{/* Right-aligned button group */}
Most earned in the past {freq_label}.
Rank
Bonus
User
{payouts?.rows?.map((dataItem: LeaderboardRow) => (
{dataItem.rank}
{formatCentsToUSD(dataItem.value)}
{truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
))}
Top completes in the past {freq_label}.
Rank
Completes
User
{completes?.rows?.map((dataItem: LeaderboardRow, index: number) => (
{dataItem.rank ??
showSmartRank(dataItem, index, completes?.rows)}
{dataItem.value}
{truncate(dataItem.bpuid, 6, "*****").toUpperCase()}
))}
);
};
type RequestState = "idle" | "working" | "failed";
interface LoginFormProps {
onSubmitSuccess: () => void;
}
const LoginForm = function ({ onSubmitSuccess }: LoginFormProps) {
const [email, setEmail] = useState("");
const [state, setState] = useState("idle");
// const dispatch = useAppDispatch();
const submit = async (event: FormEvent) => {
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 (
<>
{state === "failed" && Unable to send the link. Please try again.
}
>
);
};
const LoginSent = function () {
return (
Please check your email.
If you don't recieve anything, please check your spam folder, or try
again shortly.
);
};
const Login = () => {
const [submitted, setSubmitted] = useState(false);
return (
<>
{submitted ? (
) : (
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 (
<>
{users_f} people are actively attempting {surveys_f} available
surveys right now.
Get paid to take surveys. Our algorithm attempts to pair you with
the highest paying survey available at the moment (right now, the
highest paying survey is {survey_max_cpi_f}) that you are eligible
for. 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.
>
);
};
export default Preview;