hackatime/app/javascript/utils.ts
Mahad Kalam 28fe4739f5
Leaderboard Inertia'd + use cache + util dedup (#1121)
* make leaderboards go vrooom

* goog

* Make leaderboards great again

* Bit o' cleanup?

* goog

* goog

* Greptile
2026-03-30 14:39:05 +00:00

112 lines
3.6 KiB
TypeScript

export const pluralize = (count: number, singular: string, plural: string) =>
count === 1 ? singular : plural;
export const toSentence = (items: string[]) => {
if (items.length === 0) return "";
if (items.length === 1) return items[0];
if (items.length === 2) return `${items[0]} and ${items[1]}`;
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
};
export const secondsToDisplay = (seconds?: number) => {
if (!seconds) return "0m";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours > 0 ? (minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`) : `${minutes}m`;
};
export const secondsToDetailedDisplay = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const parts: string[] = [];
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (s > 0) parts.push(`${s}s`);
return parts.join(" ") || "0s";
};
export const secondsToCompactDisplay = (seconds?: number) => {
if (!seconds) return "0m";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours > 0 ? `${hours}h` : `${minutes}m`;
};
export const durationInWords = (seconds: number): string => {
if (seconds < 60) return "less than a minute";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `about ${hours} ${hours === 1 ? "hour" : "hours"}`;
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
};
export const percentOf = (value: number, max: number) => {
if (!max || max === 0) return 0;
return Math.max(2, Math.round((value / max) * 100));
};
export const logScale = (value: number, maxVal: number): number => {
if (value === 0) return 0;
const minPercent = 5;
const maxPercent = 100;
const linearRatio = value / maxVal;
const logRatio = Math.log(value + 1) / Math.log(maxVal + 1);
const linearWeight = 0.8;
const logWeight = 0.2;
const scaled =
minPercent +
(linearWeight * linearRatio + logWeight * logRatio) *
(maxPercent - minPercent);
return Math.min(Math.round(scaled), maxPercent);
};
export const timeAgo = (isoString: string) => {
const diff = Date.now() - new Date(isoString).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return "just now";
if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"} ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
const days = Math.floor(hours / 24);
return `${days} day${days === 1 ? "" : "s"} ago`;
};
export const streakTheme = (count: number) => {
if (count >= 30)
return {
bg: "from-blue/20 to-purple/20",
hbg: "hover:from-blue/30 hover:to-purple/30",
bc: "border-blue",
ic: "text-blue",
tc: "text-blue",
tm: "text-blue",
};
if (count >= 7)
return {
bg: "from-red/20 to-orange/20",
hbg: "hover:from-red/30 hover:to-orange/30",
bc: "border-red",
ic: "text-red",
tc: "text-red",
tm: "text-red",
};
return {
bg: "from-orange/20 to-yellow/20",
hbg: "hover:from-orange/30 hover:to-yellow/30",
bc: "border-orange",
ic: "text-orange",
tc: "text-orange",
tm: "text-orange",
};
};
export const streakLabel = (count: number) =>
count > 30 ? "30+" : `${count}`;
export const rankDisplay = (index: number) => {
if (index === 0) return "🥇";
if (index === 1) return "🥈";
if (index === 2) return "🥉";
return `${index + 1}`;
};