easy fixes

This commit is contained in:
Miodec 2025-02-14 19:54:21 +01:00
parent 71f8e9ac79
commit 86dcef01e2
27 changed files with 157 additions and 147 deletions

View file

@ -259,7 +259,6 @@ export const commands: CommandsSubgroup = {
...TribeDeltaCommands,
...TribeCaretsCommands,
//sound
...SoundVolumeCommands,
...SoundOnClickCommands,
@ -500,12 +499,12 @@ const lists = {
tags: TagsCommands[0]?.subgroup,
resultSaving: ResultSavingCommands[0]?.subgroup,
blindMode: BlindModeCommands[0]?.subgroup,
mode: ModeCommands[0].subgroup,
time: TimeCommands[0].subgroup,
words: WordsCommands[0].subgroup,
quoteLength: QuoteLengthCommands[0].subgroup,
punctuation: PunctuationCommands[0].subgroup,
numbers: NumbersCommands[0].subgroup,
mode: ModeCommands[0]?.subgroup,
time: TimeCommands[0]?.subgroup,
words: WordsCommands[0]?.subgroup,
quoteLength: QuoteLengthCommands[0]?.subgroup,
punctuation: PunctuationCommands[0]?.subgroup,
numbers: NumbersCommands[0]?.subgroup,
};
export function doesListExist(listName: string): boolean {

View file

@ -1,6 +1,7 @@
import * as UpdateConfig from "../../config";
import { Command, CommandsSubgroup } from "../types";
const subgroup: MonkeyTypes.CommandsSubgroup = {
const subgroup: CommandsSubgroup = {
title: "Tribe carets...",
configKey: "tribeCarets",
list: [
@ -31,7 +32,7 @@ const subgroup: MonkeyTypes.CommandsSubgroup = {
],
};
const commands: MonkeyTypes.Command[] = [
const commands: Command[] = [
{
id: "changeTribeCarets",
display: "Tribe carets...",

View file

@ -1,6 +1,7 @@
import * as UpdateConfig from "../../config";
import { Command, CommandsSubgroup } from "../types";
const subgroup: MonkeyTypes.CommandsSubgroup = {
const subgroup: CommandsSubgroup = {
title: "Tribe delta...",
configKey: "tribeDelta",
list: [
@ -31,7 +32,7 @@ const subgroup: MonkeyTypes.CommandsSubgroup = {
],
};
const commands: MonkeyTypes.Command[] = [
const commands: Command[] = [
{
id: "changeTribeDelta",
display: "Tribe delta...",

View file

@ -1522,22 +1522,23 @@ $("#wordsInput").on("input", (event) => {
if (popupVisible) {
event.preventDefault();
return;
}
if (
[10, 11].includes(TribeState.getState()) &&
TribeState.getSelf()?.isTyping
) {
event.preventDefault();
return;
}
//this could be wrong
if (
[10, 11].includes(TribeState.getState()) &&
TribeState.getSelf()?.isTyping
) {
event.preventDefault();
return;
}
if (
[10, 11].includes(TribeState.getState()) &&
TribeState.getSelf()?.isTyping
) {
event.preventDefault();
return;
}
if (
[10, 11].includes(TribeState.getState()) &&
TribeState.getSelf()?.isTyping
) {
event.preventDefault();
return;
}
TestInput.setCurrentNotAfk();

View file

@ -153,9 +153,9 @@ class QuotesController {
if (trulyRandom) {
if (Config.quoteLength.length === 1) {
return randomElementFromArray(
this.quoteCollection.groups[Config.quoteLength[0]]
);
const ql = Config.quoteLength[0] as number;
const group = this.quoteCollection.groups[ql] as Quote[];
return randomElementFromArray(group);
} else {
return randomElementFromArray(this.quoteCollection.quotes);
}

View file

@ -68,7 +68,7 @@ const routes: Route[] = [
navigate("/tribe", navigateOptions);
}
} else {
PageController.change("test", {
void PageController.change("test", {
tribeOverride: navigateOptions?.tribeOverride ?? false,
force: navigateOptions?.force ?? false,
});
@ -163,7 +163,7 @@ const routes: Route[] = [
path: "/tribe",
load: (params, navigateOptions): void => {
if (navigateOptions?.tribeOverride === true) {
PageController.change("tribe", {
void PageController.change("tribe", {
tribeOverride: navigateOptions?.tribeOverride ?? false,
force: navigateOptions?.force ?? false,
params,
@ -174,7 +174,7 @@ const routes: Route[] = [
if (TribeState.getState() == 22 && TribeState.getSelf()?.isLeader) {
tribeSocket.out.room.backToLobby();
} else {
PageController.change("tribe", {
void PageController.change("tribe", {
tribeOverride: navigateOptions?.tribeOverride ?? false,
force: navigateOptions?.force ?? false,
params,
@ -185,8 +185,8 @@ const routes: Route[] = [
{
path: "/tribe/:roomId",
load: (params): void => {
setAutoJoin(params["roomId"]);
PageController.change("tribe", {
setAutoJoin(params["roomId"] as string);
void PageController.change("tribe", {
force: true,
params,
});

View file

@ -11,9 +11,9 @@ let colors = {
colorfulErrorExtra: "#7e2a33",
};
export async function get(
color: keyof MonkeyTypes.ThemeColors
): Promise<string> {
export type ColorName = keyof typeof colors;
export async function get(color: ColorName): Promise<string> {
if (!colors[color]) update();
return colors[color];
}

View file

@ -27,7 +27,7 @@ type PageProperties<T> = {
name: PageName;
element: JQuery;
path: string;
beforeHide?: () => Promise<void>;
beforeHide?: (options: PageFunctionOptions) => Promise<void>;
afterHide?: () => Promise<void>;
beforeShow?: (options: Options<T>) => Promise<void>;
afterShow?: () => Promise<void>;

View file

@ -434,12 +434,12 @@ async function initGroups(): Promise<void> {
"tribeDelta",
UpdateConfig.setTribeDelta,
"button"
) as SettingsGroup<SharedTypes.ConfigValues>;
) as SettingsGroup<ConfigValue>;
groups["tribeCarets"] = new SettingsGroup(
"tribeCarets",
UpdateConfig.setTribeCarets,
"button"
) as SettingsGroup<SharedTypes.ConfigValues>;
) as SettingsGroup<ConfigValue>;
// groups.customLayoutfluid = new SettingsGroup(
// "customLayoutfluid",
// UpdateConfig.setCustomLayoutfluid

View file

@ -8,7 +8,6 @@ import { updateFooterAndVerticalAds } from "../controllers/ad-controller";
import * as ModesNotice from "../elements/modes-notice";
import * as Keymap from "../elements/keymap";
import * as TribeState from "../tribe/tribe-state";
import * as TestConfig from "../test/test-config";
export const page = new Page({
name: "test",

View file

@ -3,18 +3,18 @@ import * as Tribe from "../tribe/tribe";
import * as TribeState from "../tribe/tribe-state";
import * as TribeChat from "../tribe/tribe-chat";
export const page = new Page(
"tribe",
$(".page.pageTribe"),
"/tribe",
async () => {
export const page = new Page({
name: "tribe",
element: $(".page.pageTribe"),
path: "/tribe",
beforeHide: async () => {
// TODO: Fill it up later
},
async () => {
afterHide: async () => {
// TODO: Fill it up later
TribeChat.reset("lobby");
},
async () => {
beforeShow: async () => {
if (TribeState.getState() == 5) {
TribeChat.fill("lobby");
setTimeout(() => {
@ -22,9 +22,9 @@ export const page = new Page(
}, 50);
}
},
async () => {
afterShow: async () => {
if (TribeState.getState() < 1) {
Tribe.init();
void Tribe.init();
}
}
);
},
});

View file

@ -4,14 +4,15 @@ import tribeSocket from "../tribe/tribe-socket";
export function show(userId: string): void {
if (!userId) {
return Notifications.add(
Notifications.add(
"Cannot show user settings without passing in user id",
-1
);
return;
}
if ($("#tribeUserSettingsPopupWrapper").hasClass("hidden")) {
$("#tribeUserSettingsPopup .title").text(
`User settings (${TribeState.getRoom()?.users[userId].name})`
`User settings (${TribeState.getRoom()?.users[userId]?.name})`
);
$("#tribeUserSettingsPopup").attr("userid", userId);
$("#tribeUserSettingsPopupWrapper")
@ -49,14 +50,14 @@ $("#tribeUserSettingsPopupWrapper").on("click", (e) => {
$("#tribeUserSettingsPopup .button.banButton").on("click", () => {
const userId = $("#tribeUserSettingsPopup").attr("userid");
if (!userId) return;
if (userId === undefined) return;
tribeSocket.out.room.banUser(userId);
hide();
});
$("#tribeUserSettingsPopup .button.giveLeaderButton").on("click", () => {
const userId = $("#tribeUserSettingsPopup").attr("userid");
if (!userId) return;
if (userId === undefined) return;
tribeSocket.out.room.giveLeader(userId);
hide();
});

View file

@ -969,8 +969,8 @@ export async function update(
if (room?.users) {
for (const userId of Object.keys(room.users)) {
if (userId === TribeState.getSelf()?.id) continue;
if (room.users[userId].isFinished) {
TribeChartController.drawChart(userId);
if (room.users[userId]?.isFinished) {
void TribeChartController.drawChart(userId);
}
}
}

View file

@ -70,6 +70,7 @@ import Format from "../utils/format";
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
import {
ChartData,
CompletedEvent,
CustomTextDataWithTextLen,
} from "@monkeytype/contracts/schemas/results";
@ -687,9 +688,9 @@ export async function retrySavingResult(): Promise<void> {
Notifications.add("Retrying to save...");
const tribeChartData = {
wpm: [...(completedEvent.chartData as MonkeyTypes.ChartData).wpm],
raw: [...(completedEvent.chartData as MonkeyTypes.ChartData).raw],
err: [...(completedEvent.chartData as MonkeyTypes.ChartData).err],
wpm: [...(completedEvent.chartData as ChartData).wpm],
raw: [...(completedEvent.chartData as ChartData).raw],
err: [...(completedEvent.chartData as ChartData).err],
};
await saveResult(completedEvent, tribeChartData, true);
@ -1127,11 +1128,11 @@ export async function finish(difficultyFailed = false): Promise<void> {
);
Result.updateTodayTracker();
if (completedEvent.bailedOut === true) {
if (completedEvent.bailedOut) {
resolve.bailedOut = true;
}
if (completedEvent.bailedOut === true) {
if (completedEvent.bailedOut) {
resolve.bailedOut = true;
}
@ -1174,9 +1175,9 @@ export async function finish(difficultyFailed = false): Promise<void> {
}
const tribeChartData = {
wpm: [...(completedEvent.chartData as MonkeyTypes.ChartData).wpm],
raw: [...(completedEvent.chartData as MonkeyTypes.ChartData).raw],
err: [...(completedEvent.chartData as MonkeyTypes.ChartData).err],
wpm: [...(completedEvent.chartData as ChartData).wpm],
raw: [...(completedEvent.chartData as ChartData).raw],
err: [...(completedEvent.chartData as ChartData).err],
};
if (completedEvent.testDuration > 122) {
@ -1188,7 +1189,7 @@ export async function finish(difficultyFailed = false): Promise<void> {
if (dontSave) {
void AnalyticsController.log("testCompletedInvalid");
resolveTestSavePromise(resolve);
TribeResults.send({
void TribeResults.send({
wpm: completedEvent.wpm,
raw: completedEvent.rawWpm,
acc: completedEvent.acc,
@ -1221,7 +1222,7 @@ export async function finish(difficultyFailed = false): Promise<void> {
async function saveResult(
completedEvent: CompletedEvent,
tribeChartData: SharedTypes.ChartData,
tribeChartData: ChartData,
isRetrying: boolean
): Promise<void> {
if (!TestState.savingEnabled) {
@ -1234,7 +1235,7 @@ async function saveResult(
resolve.saved = false;
resolve.saveFailedMessage = "Disabled by user";
resolveTestSavePromise(resolve);
TribeResults.send({
void TribeResults.send({
wpm: completedEvent.wpm,
raw: completedEvent.rawWpm,
acc: completedEvent.acc,
@ -1257,7 +1258,7 @@ async function saveResult(
resolve.saved = false;
resolve.saveFailedMessage = "Offline";
resolveTestSavePromise(resolve);
TribeResults.send({
void TribeResults.send({
wpm: completedEvent.wpm,
raw: completedEvent.rawWpm,
acc: completedEvent.acc,
@ -1293,9 +1294,9 @@ async function saveResult(
}
resolve.login = true;
resolve.saved = false;
resolve.saveFailedMessage = response.message;
resolve.saveFailedMessage = response.body.message;
resolveTestSavePromise(resolve);
TribeResults.send({
void TribeResults.send({
wpm: completedEvent.wpm,
raw: completedEvent.rawWpm,
acc: completedEvent.acc,
@ -1433,7 +1434,7 @@ async function saveResult(
resolve.login = true;
resolve.saved = true;
resolve.isPb = response?.data?.isPb ?? false;
resolve.isPb = response.body.data?.isPb ?? false;
resolveTestSavePromise(resolve);
@ -1442,7 +1443,7 @@ async function saveResult(
Notifications.add("Result saved", 1, { important: true });
}
TribeResults.send({
void TribeResults.send({
wpm: completedEvent.wpm,
raw: completedEvent.rawWpm,
acc: completedEvent.acc,

View file

@ -589,27 +589,19 @@ function updateWordsHeight(force = false): void {
$("#wordsWrapper").css({ overflow: "visible clip" });
}
$("#wordsWrapper")
.css("height", finalWrapperHeight + "px")
.css("overflow", "hidden");
$(".outOfFocusWarning").css(
"margin-top",
finalWrapperHeight / 2 - Numbers.convertRemToPixels(1) / 2 + "px"
);
setTimeout(() => {
$("#words").css("height", finalWordsHeight + "px");
$("#wordsWrapper").css("height", finalWrapperHeight + "px");
$(".outOfFocusWarning").css(
"margin-top",
finalWrapperHeight / 2 - convertRemToPixels(1) / 2 + "px"
);
$("#typingTest .tribeCountdown").css(
"line-height",
finalWrapperHeight + "px"
);
}, 0);
}
setTimeout(() => {
$("#words").css("height", finalWordsHeight + "px");
$("#wordsWrapper").css("height", finalWrapperHeight + "px");
$(".outOfFocusWarning").css(
"margin-top",
finalWrapperHeight / 2 - convertRemToPixels(1) / 2 + "px"
);
$("#typingTest .tribeCountdown").css(
"line-height",
finalWrapperHeight + "px"
);
}, 0);
}
export function addWord(word: string): void {
@ -673,7 +665,7 @@ export async function screenshot(): Promise<void> {
for (const fb of getActiveFunboxes()) {
fb.functions?.applyGlobalCSS?.();
}
if (TribeState.getState() > 5) {
$(".pageTest #result .inviteLink").removeClass("hidden");
}
@ -1660,17 +1652,6 @@ $("#wordsInput").on("focusout", () => {
Caret.hide();
});
$(document).on("keypress", "#practiseWordsButton", (event) => {
if (event.keyCode == 13) {
PractiseWords.showPopup(true);
}
});
$(document.body).on("click", "#practiseWordsButton", () => {
// PractiseWords.init();
PractiseWords.showPopup();
});
$(".pageTest").on("click", "#showWordHistoryButton", () => {
toggleResultWords();
});

View file

@ -14,10 +14,6 @@ import * as Misc from "../utils/misc";
import * as Strings from "../utils/strings";
import * as Arrays from "../utils/arrays";
import * as TestState from "../test/test-state";
import * as GetText from "../utils/generate";
import * as Random from "../utils/random";
import * as TribeState from "../tribe/tribe-state";
import * as GetText from "../utils/generate";
import * as Random from "../utils/random";
import * as TribeState from "../tribe/tribe-state";
import * as GetText from "../utils/generate";

View file

@ -132,19 +132,20 @@ export function updateRoomConfig(): void {
} else if (room.config.mode === "custom") {
let t = "Custom settings:";
t += `\ntext length: ${CustomText.text.length}`;
if (CustomText.isTimeRandom || CustomText.isWordRandom) {
let r = "";
let n = "";
if (CustomText.isTimeRandom) {
r = "time";
n = CustomText.time.toString();
} else if (CustomText.isWordRandom) {
r = "words";
n = CustomText.word.toString();
}
t += `\nrandom: ${r} ${n}`;
}
t += `\ntext length: ${CustomText.getText().length}`;
//todo
// if (CustomText.isTimeRandom || CustomText.isWordRandom) {
// let r = "";
// let n = "";
// if (CustomText.isTimeRandom) {
// r = "time";
// n = CustomText.time.toString();
// } else if (CustomText.isWordRandom) {
// r = "words";
// n = CustomText.word.toString();
// }
// t += `\nrandom: ${r} ${n}`;
// }
$(".pageTribe .tribePage.lobby .currentConfig .groups").append(`
<div class='group' aria-label="${t}" data-balloon-pos="up" data-balloon-break command="changeCustomText">

View file

@ -49,7 +49,7 @@ function refreshQueueButtons(): void {
queues.forEach((queue, id) => {
if (queue) {
atleastone = true;
$(buttons[id]).addClass("active");
$(buttons[id] as HTMLElement).addClass("active");
}
});
if (!atleastone) {

View file

@ -108,7 +108,7 @@ export function update(page: string, userId: string): void {
}
const user = room.users[userId];
if (!el) {
if (!el || !user) {
return;
}
@ -162,7 +162,7 @@ export function completeBar(page: string, userId: string): void {
export function fadeUser(
page: string | undefined,
userId: string,
changeColor?: keyof MonkeyTypes.ThemeColors
changeColor?: ThemeColors.ColorName
): void {
if (page === undefined) {
fadeUser("test", userId, changeColor);
@ -181,9 +181,9 @@ export function fadeUser(
el.find(`.player[id=${userId}]`).addClass("faded");
if (changeColor) {
ThemeColors.get(changeColor).then((color) => {
if (!el) return;
if (changeColor !== undefined) {
void ThemeColors.get(changeColor).then((color) => {
if (el === undefined) return;
el.find(`.player[id=${userId}] .bar`).css("background-color", color);
});
}

View file

@ -3,7 +3,7 @@ import Config from "../config";
import * as TestState from "../test/test-state";
import tribeSocket from "./tribe-socket";
import * as ConfigEvent from "../observables/config-event";
import { mapRange } from "../utils/misc";
import { mapRange } from "@monkeytype/util/numbers";
const el = $(".pageTest #miniTimerAndLiveWpm .tribeDelta");
const elBar = $(".pageTest #tribeDeltaBar");

View file

@ -1,9 +1,9 @@
import * as TribeState from "./tribe-state";
import * as Misc from "../utils/misc";
import Config from "../config";
import * as SlowTimer from "../states/slow-timer";
import tribeSocket from "./tribe-socket";
import { FinalPositions } from "./tribe-socket/routes/room";
import { getOrdinalNumberString } from "@monkeytype/util/numbers";
const initialised: Record<string, boolean | object> = {};
@ -149,7 +149,7 @@ export function updatePositions(
const userEl = $(
`.pageTest #result #tribeResults table tbody tr.user[id="${user.id}"]`
);
const string = Misc.getPositionString(parseInt(position));
const string = getOrdinalNumberString(parseInt(position));
userEl.find(".pos").text(string);
userEl
.find(".points")
@ -164,7 +164,7 @@ export function updatePositions(
const el = $(".pageTest #result #tribeResults table tbody");
el.find("tr.user").each((_, userEl) => {
const id = $(userEl).attr("id");
if (id) {
if (id !== undefined) {
elements[id] = $(userEl);
}
});
@ -174,12 +174,13 @@ export function updatePositions(
for (const [_pos, users] of Object.entries(positions)) {
for (const user of users) {
el.append(elements[user.id]);
el.append(elements[user.id] as JQuery<HTMLElement>);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete elements[user.id];
}
}
for (const id of Object.keys(elements)) {
el.append(elements[id]);
el.append(elements[id] as JQuery<HTMLElement>);
}
}
}
@ -261,13 +262,13 @@ function updateUser(page: string, userId: string): void {
otherText = "afk";
} else if (resolve.repeated) {
otherText = "repeated";
} else if (resolve.failed && room.config.isInfiniteTest === false) {
} else if (resolve.failed && !room.config.isInfiniteTest) {
otherText = `failed(${resolve.failedReason})`;
} else if (resolve.saved === false) {
otherText = `save failed(${resolve.saveFailedMessage})`;
} else if (resolve.valid === false) {
otherText = `invalid`;
} else if (room.config.isInfiniteTest === true) {
} else if (room.config.isInfiniteTest) {
otherText = `${Math.round(userResult.testDuration)}s`;
} else if (resolve.saved && resolve.isPb) {
otherText = "new pb";
@ -280,8 +281,8 @@ function updateUser(page: string, userId: string): void {
export function update(page: string, userId?: string): void {
const room = TribeState.getRoom();
if (!room) return;
if (!initialised[page]) init(page);
if (userId) {
if (initialised[page] === undefined) init(page);
if (userId !== undefined) {
updateUser(page, userId);
} else {
for (const [userId, user] of Object.entries(room.users)) {

View file

@ -1,4 +1,6 @@
import { Mode } from "@monkeytype/contracts/schemas/shared";
import Socket from "../socket";
import { QuoteLength } from "@monkeytype/contracts/schemas/configs";
async function getPublicRooms(
_page: number,
@ -86,8 +88,8 @@ function result(result: TribeTypes.Result): void {
}
function create(
mode: MonkeyTypes.Mode,
mode2: string | number | MonkeyTypes.QuoteLength[],
mode: Mode,
mode2: string | number | QuoteLength[],
type?: string
): void {
Socket.emit("room_create", { mode, mode2, type });

View file

@ -24,6 +24,6 @@ export function play(name: string): void {
if (!TribeState.getSelf()?.isTyping && ["cd", "cd_go"].includes(name)) {
return;
}
sounds[name].seek(0);
sounds[name].play();
sounds[name]?.seek(0);
sounds[name]?.play();
}

View file

@ -13,7 +13,9 @@ export function hideLoading(): void {
export function updateQueueButtons(): void {
const buttons = $(".pageTribe .menu .matchmaking .buttons .button");
inQueueNumbers.forEach((num, index) => {
$(buttons[index]).find(".subtext .waiting").text(`Waiting: ${num}`);
$(buttons[index] as HTMLElement)
.find(".subtext .waiting")
.text(`Waiting: ${num}`);
});
}
@ -41,12 +43,16 @@ export function updateMenuButtons(
): void {
let buttons = $(".pageTribe .menu .matchmaking .buttons .button");
races.mm.forEach((num: number, index: number) => {
$(buttons[index]).find(".subtext .races").text(`Races: ${num}`);
$(buttons[index] as HTMLElement)
.find(".subtext .races")
.text(`Races: ${num}`);
});
buttons = $(".pageTribe .menu .customRooms .buttons .button");
races.custom.forEach((num: number, index: number) => {
$(buttons[index]).find(".subtext .rooms").text(`Rooms: ${num}`);
$(buttons[index] as HTMLElement)
.find(".subtext .rooms")
.text(`Rooms: ${num}`);
});
}
@ -55,7 +61,7 @@ let to: NodeJS.Timeout | null = null;
export async function refresh(): Promise<void> {
showLoading();
tribeSocket.out.system.stats(performance.now()).then((data) => {
void tribeSocket.out.system.stats(performance.now()).then((data) => {
const ping = Math.round(performance.now() - data.pingStart);
hideLoading();
setInQueue(data.stats[2]);
@ -78,7 +84,7 @@ export async function refresh(): Promise<void> {
) {
to = setTimeout(() => {
to = null;
refresh();
void refresh();
}, 15000);
}
}

View file

@ -1,4 +1,5 @@
import { roundTo2 } from "@monkeytype/util/numbers";
import * as Random from "./random";
/**
* Converts a value in rem units to pixels based on the root element's font size.

View file

@ -408,6 +408,7 @@ export const ConfigGroupNameSchema = z.enum([
"hideElements",
"ads",
"hidden",
"tribe",
]);
export type ConfigGroupName = z.infer<typeof ConfigGroupNameSchema>;
@ -500,6 +501,8 @@ export const ConfigGroupsLiteral = {
lazyMode: "input",
showAverage: "hideElements",
maxLineWidth: "appearance",
tribeDelta: "tribe",
tribeCarets: "tribe",
} as const satisfies Record<ConfigKey, ConfigGroupName>;
export type ConfigGroups = typeof ConfigGroupsLiteral;

View file

@ -125,3 +125,19 @@ export function mapRange(
return result;
}
const suffixes = ["th", "st", "nd", "rd"];
/**
* Gets the ordinal number string for a number.
* @param number The number to get the ordinal number string for.
* @returns The ordinal number string.
* @example getOrdinalNumberString(1) // "1st"
* @example getOrdinalNumberString(2) // "2nd"
* @returns The ordinal number string.
**/
export function getOrdinalNumberString(number: number): string {
const lastTwo = number % 100;
const suffix =
suffixes[(lastTwo - 20) % 10] ?? suffixes[lastTwo] ?? suffixes[0];
return `${number}${suffix}`;
}