Take Remeda in use replacing just-utils and own utils

This commit is contained in:
Kalle
2025-04-09 22:14:11 +03:00
parent 44d740b3e2
commit 994cb97346
34 changed files with 156 additions and 248 deletions

View File

@@ -1,8 +1,7 @@
import { faker } from "@faker-js/faker";
import { sub } from "date-fns";
import capitalize from "just-capitalize";
import shuffle from "just-shuffle";
import { nanoid } from "nanoid";
import * as R from "remeda";
import { ADMIN_DISCORD_ID, ADMIN_ID, INVITE_CODE_LENGTH } from "~/constants";
import { db, sql } from "~/db/sql";
import type { SeedVariation } from "~/features/api-private/routes/seed";
@@ -66,7 +65,7 @@ import {
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator";
import { SENDOUQ_DEFAULT_MAPS } from "~/modules/tournament-map-list-generator/constants";
import { nullFilledArray, pickRandomItem } from "~/utils/arrays";
import { nullFilledArray } from "~/utils/arrays";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { mySlugify } from "~/utils/urls";
@@ -365,7 +364,7 @@ async function userProfiles() {
if (id === ADMIN_ID || id === NZAP_TEST_ID) continue;
if (Math.random() < 0.15) continue; // 85% have weapons
const weapons = shuffle([...mainWeaponIds]);
const weapons = R.shuffle(mainWeaponIds);
for (let j = 0; j < faker.helpers.arrayElement([1, 2, 3, 4, 5]); j++) {
sql
@@ -430,7 +429,7 @@ const randomPreferences = (): UserMapModePreferences => {
return {
mode,
stages: shuffle([...stageIds])
stages: R.shuffle(stageIds)
.filter((stageId) => !BANNED_MAPS[mode].includes(stageId))
.slice(0, AMOUNT_OF_MAPS_IN_POOL_PER_MODE),
};
@@ -457,7 +456,7 @@ async function userQWeaponPool() {
if (id === 2) continue; // no weapons for N-ZAP
if (Math.random() < 0.2) continue; // 80% have weapons
const weapons = shuffle([...mainWeaponIds]).slice(
const weapons = R.shuffle(mainWeaponIds).slice(
0,
faker.helpers.arrayElement([1, 2, 3, 4]),
);
@@ -599,7 +598,7 @@ function syncPlusTiers() {
}
function badgesToAdmin() {
const availableBadgeIds = shuffle(
const availableBadgeIds = R.shuffle(
(sql.prepare(`select "id" from "Badge"`).all() as any[]).map((b) => b.id),
).slice(0, 8) as number[];
@@ -619,7 +618,7 @@ function badgesToAdmin() {
}
function getAvailableBadgeIds() {
return shuffle(
return R.shuffle(
(sql.prepare(`select "id" from "Badge"`).all() as any[]).map((b) => b.id),
);
}
@@ -634,7 +633,7 @@ function badgesToUsers() {
).map((u) => u.id) as number[];
for (const id of availableBadgeIds) {
userIds = shuffle(userIds);
userIds = R.shuffle(userIds);
for (
let i = 0;
i <
@@ -720,7 +719,7 @@ function calendarEvents() {
const userIds = userIdsInRandomOrder();
for (let id = 1; id <= AMOUNT_OF_CALENDAR_EVENTS; id++) {
const shuffledTags = shuffle(Object.keys(persistedTags));
const shuffledTags = R.shuffle(Object.keys(persistedTags));
sql
.prepare(
@@ -746,7 +745,7 @@ function calendarEvents() {
)
.run({
id,
name: `${capitalize(faker.word.adjective())} ${capitalize(
name: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
faker.word.noun(),
)}`,
description: faker.lorem.paragraph(),
@@ -861,7 +860,7 @@ async function calendarEventResults() {
.fill(null)
.map((_, i) => ({
placement: i + 1,
teamName: capitalize(faker.word.noun()),
teamName: R.capitalize(faker.word.noun()),
players: new Array(
faker.helpers.arrayElement([1, 2, 3, 4, 4, 4, 4, 4, 5, 6]),
)
@@ -1321,7 +1320,7 @@ function calendarEventWithToToolsTeams(
event !== "LUTI" &&
(Math.random() < 0.8 || id === 1)
) {
const shuffledPairs = shuffle(availablePairs.slice());
const shuffledPairs = R.shuffle(availablePairs.slice());
let SZ = 0;
let TC = 0;
@@ -1410,7 +1409,7 @@ function tournamentSubs() {
)
.map(() => {
while (true) {
const weaponId = pickRandomItem(mainWeaponIds);
const weaponId = R.sample(mainWeaponIds, 1)[0]!;
if (!includedWeaponIds.includes(weaponId)) {
includedWeaponIds.push(weaponId);
return weaponId;
@@ -1426,7 +1425,7 @@ function tournamentSubs() {
)
.map(() => {
while (true) {
const weaponId = pickRandomItem(mainWeaponIds);
const weaponId = R.sample(mainWeaponIds, 1)[0]!;
if (!includedWeaponIds.includes(weaponId)) {
includedWeaponIds.push(weaponId);
return weaponId;
@@ -1443,7 +1442,7 @@ function tournamentSubs() {
}
const randomAbility = (legalTypes: AbilityType[]) => {
const randomOrderAbilities = shuffle([...abilities]);
const randomOrderAbilities = R.shuffle([...abilities]);
return randomOrderAbilities.find((a) => legalTypes.includes(a.type))!.name;
};
@@ -1451,16 +1450,16 @@ const randomAbility = (legalTypes: AbilityType[]) => {
const adminWeaponPool = mainWeaponIds.filter(() => Math.random() > 0.8);
async function adminBuilds() {
for (let i = 0; i < 50; i++) {
const randomOrderHeadGear = shuffle(headGearIds.slice());
const randomOrderClothesGear = shuffle(clothesGearIds.slice());
const randomOrderShoesGear = shuffle(shoesGearIds.slice());
const randomOrderHeadGear = R.shuffle(headGearIds.slice());
const randomOrderClothesGear = R.shuffle(clothesGearIds.slice());
const randomOrderShoesGear = R.shuffle(shoesGearIds.slice());
// filter out sshot to prevent test flaking
const randomOrderWeaponIds = shuffle(
const randomOrderWeaponIds = R.shuffle(
adminWeaponPool.filter((id) => id !== 40).slice(),
);
await BuildRepository.create({
title: `${capitalize(faker.word.adjective())} ${capitalize(
title: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
faker.word.noun(),
)}`,
ownerId: ADMIN_ID,
@@ -1514,10 +1513,10 @@ async function manySplattershotBuilds() {
for (let i = 0; i < 499; i++) {
const SPLATTERSHOT_ID = 40;
const randomOrderHeadGear = shuffle(headGearIds.slice());
const randomOrderClothesGear = shuffle(clothesGearIds.slice());
const randomOrderShoesGear = shuffle(shoesGearIds.slice());
const randomOrderWeaponIds = shuffle(mainWeaponIds.slice()).filter(
const randomOrderHeadGear = R.shuffle(headGearIds.slice());
const randomOrderClothesGear = R.shuffle(clothesGearIds.slice());
const randomOrderShoesGear = R.shuffle(shoesGearIds.slice());
const randomOrderWeaponIds = R.shuffle(mainWeaponIds.slice()).filter(
(id) => id !== SPLATTERSHOT_ID,
);
@@ -1525,7 +1524,7 @@ async function manySplattershotBuilds() {
await BuildRepository.create({
private: 0,
title: `${capitalize(faker.word.adjective())} ${capitalize(
title: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
faker.word.noun(),
)}`,
ownerId,
@@ -1642,7 +1641,7 @@ function otherTeams() {
const teamName =
i === 3
? "Team Olive"
: `${capitalize(faker.word.adjective())} ${capitalize(
: `${R.capitalize(faker.word.adjective())} ${R.capitalize(
faker.word.noun(),
)}`;
const teamCustomUrl = mySlugify(teamName);
@@ -1845,7 +1844,7 @@ function xRankPlacements() {
function userFavBadges() {
// randomly choose Sendou's favorite badge
const badgeList = shuffle(
const badgeList = R.shuffle(
(
sql
.prepare(
@@ -2042,13 +2041,15 @@ const randomMapList = (
): TournamentMapListMap[] => {
const szOnly = faker.helpers.arrayElement([true, false]);
let modePattern = shuffle([...modesShort]).filter(() => Math.random() > 0.15);
let modePattern = R.shuffle([...modesShort]).filter(
() => Math.random() > 0.15,
);
if (modePattern.length === 0) {
modePattern = shuffle([...rankedModesShort]);
modePattern = R.shuffle([...rankedModesShort]);
}
const mapList: TournamentMapListMap[] = [];
const stageIdsShuffled = shuffle([...stageIds]);
const stageIdsShuffled = R.shuffle([...stageIds]);
for (let i = 0; i < 7; i++) {
const mode = modePattern.pop()!;
@@ -2071,7 +2072,7 @@ const AMOUNT_OF_USERS_WITH_SKILLS = 100;
async function playedMatches() {
const _groupMembers = (() => {
return new Array(AMOUNT_OF_USERS_WITH_SKILLS).fill(null).map(() => {
const users = shuffle(
const users = R.shuffle(
userIdsInAscendingOrderById().slice(0, AMOUNT_OF_USERS_WITH_SKILLS),
);
@@ -2082,14 +2083,14 @@ async function playedMatches() {
userIdsInAscendingOrderById()
.slice(0, AMOUNT_OF_USERS_WITH_SKILLS)
.map((id) => {
const weapons = shuffle([...mainWeaponIds]);
const weapons = R.shuffle([...mainWeaponIds]);
return [id, weapons[0]];
}),
);
let matchDate = new Date(Date.UTC(2023, 9, 15, 0, 0, 0, 0));
for (let i = 0; i < MATCHES_COUNT; i++) {
const groupMembers = shuffle([..._groupMembers]);
const groupMembers = R.shuffle([..._groupMembers]);
const groupAlphaMembers = groupMembers.pop()!;
invariant(groupAlphaMembers, "groupAlphaMembers not found");
@@ -2229,7 +2230,7 @@ async function playedMatches() {
mainWeaponIds.find((id) => id > defaultWeapons[mu.user]) ?? 0
);
const shuffled = shuffle([...mainWeaponIds]);
const shuffled = R.shuffle([...mainWeaponIds]);
return shuffled[0];
};

View File

@@ -1,3 +1,5 @@
import { nanoid } from "nanoid";
import * as R from "remeda";
import {
AUTO_BOMB_ID,
type Ability,
@@ -21,12 +23,7 @@ import {
POINT_SENSOR_ID,
} from "~/modules/in-game-lists";
import invariant from "~/utils/invariant";
import {
cutToNDecimalPlaces,
roundToNDecimalPlaces,
sumArray,
} from "~/utils/number";
import { semiRandomId } from "~/utils/strings";
import { cutToNDecimalPlaces, roundToNDecimalPlaces } from "~/utils/number";
import { assertUnreachable } from "~/utils/types";
import {
DAMAGE_TYPE,
@@ -289,7 +286,7 @@ export function fullInkTankOptions(
if (typeof mainWeaponInkConsume !== "number") continue;
result.push({
id: semiRandomId(),
id: nanoid(),
subsUsed: subsFromFullInkTank,
type,
value: effectToRounded(
@@ -463,7 +460,7 @@ function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] {
type,
value: subValue.Damage / 10,
distance: subValue.Distance,
id: semiRandomId(),
id: nanoid(),
multiShots: multiShot[args.weaponSplId],
});
}
@@ -474,7 +471,7 @@ function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] {
if (typeof value !== "number") continue;
result.push({
id: semiRandomId(),
id: nanoid(),
type,
value: value / 10,
shotsToSplat: shotsToSplat({
@@ -505,7 +502,7 @@ function specialWeaponDamages(
type,
value: subValue.Damage / 10,
distance: subValue.Distance,
id: semiRandomId(),
id: nanoid(),
multiShots: multiShot[args.weaponSplId],
});
}
@@ -516,7 +513,7 @@ function specialWeaponDamages(
if (typeof value !== "number") continue;
result.push({
id: semiRandomId(),
id: nanoid(),
type,
value: value / 10,
shotsToSplat: shotsToSplat({
@@ -532,9 +529,9 @@ function specialWeaponDamages(
// Artifically combined damages
if (args.mainWeaponParams.specialWeaponId === ZIPCASTER_ID) {
result.unshift({
id: semiRandomId(),
id: nanoid(),
distance: 0,
value: sumArray(result.map((v) => v.value)),
value: R.sum(result.map((v) => v.value)),
type: result[0].type,
});
}
@@ -545,9 +542,9 @@ function specialWeaponDamages(
);
result.splice(firstCannonDamageIdx, 0, {
id: semiRandomId(),
id: nanoid(),
distance: 0,
value: sumArray(cannonDamages.map((v) => v.value)),
value: R.sum(cannonDamages.map((v) => v.value)),
type: "SPECIAL_CANNON",
});
}
@@ -604,7 +601,7 @@ function subWeaponDefenseDamages(
params: args.subWeaponParams,
}),
distance: subValue.Distance,
id: semiRandomId(),
id: nanoid(),
subWeaponId: id,
});
}
@@ -612,12 +609,12 @@ function subWeaponDefenseDamages(
// Burst Bomb direct damage
if (id === BURST_BOMB_ID) {
arrayValues.unshift({
id: semiRandomId(),
id: nanoid(),
subWeaponId: id,
distance: 0,
baseValue: sumArray(arrayValues.map((v) => v.baseValue)),
baseValue: R.sum(arrayValues.map((v) => v.baseValue)),
value: cutToNDecimalPlaces(
sumArray(arrayValues.map((v) => v.value)),
R.sum(arrayValues.map((v) => v.value)),
1,
),
type,
@@ -639,7 +636,7 @@ function subWeaponDefenseDamages(
arrayValues = [
{
id: semiRandomId(),
id: nanoid(),
subWeaponId: id,
distance: [
Math.min(
@@ -658,7 +655,7 @@ function subWeaponDefenseDamages(
type,
},
{
id: semiRandomId(),
id: nanoid(),
subWeaponId: id,
distance: [
Math.min(
@@ -683,7 +680,7 @@ function subWeaponDefenseDamages(
if (typeof value !== "number") continue;
result.push({
id: semiRandomId(),
id: nanoid(),
type,
baseValue: value / 10,
value: subWeaponDamageValue({

View File

@@ -32,7 +32,7 @@ import {
abilitiesShort,
isAbility,
} from "~/modules/in-game-lists";
import { atOrError, nullFilledArray, removeDuplicates } from "~/utils/arrays";
import { atOrError, nullFilledArray } from "~/utils/arrays";
import { damageTypeTranslationString } from "~/utils/i18next";
import invariant from "~/utils/invariant";
import type { SendouRouteHandle } from "~/utils/remix.server";
@@ -79,6 +79,7 @@ import {
isStackableAbility,
} from "../core/utils";
import "../analyzer.css";
import * as R from "remeda";
import { SendouSwitch } from "~/components/elements/Switch";
export const CURRENT_PATCH = "9.3";
@@ -1117,7 +1118,7 @@ function subDefenseGraphOptions({
}),
);
const distanceKeys = removeDuplicates(
const distanceKeys = R.unique(
analyzedBuilds[0].stats.subWeaponDefenseDamages
.filter((d) => (d as SubWeaponDamage).subWeaponId === subWeaponId)
.filter((d) => d.value < 100)

View File

@@ -1,6 +1,7 @@
import type { Expression, ExpressionBuilder, Transaction } from "kysely";
import { sql } from "kysely";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import * as R from "remeda";
import { db } from "~/db/sql";
import type {
CalendarEventTag,
@@ -13,7 +14,6 @@ import { MapPool } from "~/features/map-list-generator/core/map-pool";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { sumArray } from "~/utils/number";
import type { Unwrapped } from "~/utils/types";
// TODO: convert from raw to using the "exists" function
@@ -317,7 +317,7 @@ async function tournamentParticipantCount({
return {
teams: rows.length,
players: sumArray(rows.map((row) => row.memberCount)),
players: R.sum(rows.map((row) => row.memberCount)),
};
}

View File

@@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next";
import * as R from "remeda";
import { Button } from "~/components/Button";
import { WeaponCombobox } from "~/components/Combobox";
import { WeaponImage } from "~/components/Image";
@@ -9,7 +10,6 @@ import type { TierName } from "~/features/mmr/mmr-constants";
import { TIERS } from "~/features/mmr/mmr-constants";
import { languagesUnified } from "~/modules/i18n/config";
import type { MainWeaponId } from "~/modules/in-game-lists";
import { capitalize } from "~/utils/strings";
import { LFG } from "../lfg-constants";
import type { LFGFilter } from "../lfg-types";
@@ -287,7 +287,7 @@ function TierFilterFields({
>
{TIERS.map((tier) => (
<option key={tier.name} value={tier.name}>
{capitalize(tier.name.toLowerCase())}
{R.capitalize(tier.name.toLowerCase())}
</option>
))}
</select>

View File

@@ -1,4 +1,4 @@
import shuffle from "just-shuffle";
import * as R from "remeda";
import type { ModeShort } from "../../../../modules/in-game-lists";
export function modesOrder(
@@ -6,10 +6,10 @@ export function modesOrder(
modes: ModeShort[],
): ModeShort[] {
if (type === "EQUAL") {
return shuffle(modes);
return R.shuffle(modes);
}
const withoutSZ = shuffle(modes.filter((mode) => mode !== "SZ"));
const withoutSZ = R.shuffle(modes.filter((mode) => mode !== "SZ"));
return withoutSZ.flatMap((mode) => [mode, "SZ"]);
}

View File

@@ -15,9 +15,9 @@ import type {
TLUiStylePanelProps,
} from "@tldraw/tldraw";
import clsx from "clsx";
import randomInt from "just-random-integer";
import * as React from "react";
import { useTranslation } from "react-i18next";
import * as R from "remeda";
import { usePlannerBg } from "~/hooks/usePlannerBg";
import type { LanguageCode } from "~/modules/i18n/config";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
@@ -156,8 +156,8 @@ export default function Planner() {
size: [imageSizePx, imageSizePx],
isLocked: false,
point: [
randomInt(imageSpawnBoxLeft, imageSpawnBoxRight),
randomInt(imageSpawnBoxTop, imageSpawnBoxBottom),
R.randomInteger(imageSpawnBoxLeft, imageSpawnBoxRight),
R.randomInteger(imageSpawnBoxTop, imageSpawnBoxBottom),
],
cb: () => editor?.setCurrentTool("select"),
});

View File

@@ -1,3 +1,4 @@
import * as R from "remeda";
import type {
AbilityPoints,
AnalyzedBuild,
@@ -10,7 +11,6 @@ import type {
SpecialWeaponId,
SubWeaponId,
} from "~/modules/in-game-lists";
import { removeDuplicates } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import { roundToNDecimalPlaces } from "~/utils/number";
import {
@@ -135,9 +135,7 @@ export function resolveAllUniqueDamageTypes({
? analyzed.stats.specialWeaponDamages.map((d) => d.type)
: analyzed.stats.damages.map((d) => d.type);
return removeDuplicates(damageTypes).filter(
(dmg) => !dmg.includes("SECONDARY"),
);
return R.unique(damageTypes).filter((dmg) => !dmg.includes("SECONDARY"));
}
function resolveFilteredDamages({

View File

@@ -1,5 +1,5 @@
import shuffle from "just-shuffle";
import { type InferResult, sql } from "kysely";
import * as R from "remeda";
import { db } from "~/db/sql";
import type { Tables, TablesInsertable } from "~/db/tables";
import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
@@ -126,7 +126,7 @@ export async function usersForVoting(loggedInUser: {
});
}
return shuffle(result.filter(({ user }) => user.id !== loggedInUser.id));
return R.shuffle(result.filter(({ user }) => user.id !== loggedInUser.id));
}
export async function hasVoted(args: {

View File

@@ -1,4 +1,4 @@
import shuffle from "just-shuffle";
import * as R from "remeda";
import type { ParsedMemento, UserMapModePreferences } from "~/db/tables";
import {
type DbMapPoolList,
@@ -100,7 +100,7 @@ export function mapLottery(
const mapPoolList: DbMapPoolList = [];
for (const mode of modes) {
const stageIdsFromPools = shuffle(
const stageIdsFromPools = R.shuffle(
preferences.flatMap((preference) => {
// if they disliked the mode don't include their maps
// they are just saved in the DB so they can be restored later
@@ -178,7 +178,7 @@ export function mapModePreferencesToModeList(
combinedMap.set(mode, combinedScore);
}
const result = shuffle(modesShort).filter((mode) => {
const result = R.shuffle(modesShort).filter((mode) => {
const score = combinedMap.get(mode)!;
// if opinion is split, don't include
@@ -196,7 +196,7 @@ export function mapModePreferencesToModeList(
if (result.length === 0) {
const bestScore = Math.max(...combinedMap.values());
const leastWorstModesResult = shuffle(modesShort).filter((mode) => {
const leastWorstModesResult = R.shuffle(modesShort).filter((mode) => {
// turf war never included if not positive
if (mode === "TW") return false;

View File

@@ -1,7 +1,7 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import * as R from "remeda";
import type { UserWithPlusTier } from "~/db/tables";
import { getUserId } from "~/features/auth/core/user.server";
import { sumArray } from "~/utils/number";
import * as TeamRepository from "../TeamRepository.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
@@ -55,7 +55,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const membersToCommonPlusTierRating = (
members: Pick<UserWithPlusTier, "plusTier">[],
) => {
return sumArray(
return R.sum(
members
.map((m) => m.plusTier ?? 100)
.sort((a, b) => a - b)

View File

@@ -3,6 +3,7 @@ import { Link, useFetcher, useLoaderData } from "@remix-run/react";
import clsx from "clsx";
import React from "react";
import { useTranslation } from "react-i18next";
import * as R from "remeda";
import { Avatar } from "~/components/Avatar";
import { Button, LinkButton } from "~/components/Button";
import { Flag } from "~/components/Flag";
@@ -16,7 +17,6 @@ import { StarIcon } from "~/components/icons/Star";
import { UsersIcon } from "~/components/icons/Users";
import { useUser } from "~/features/auth/core/user";
import { isAdmin } from "~/permissions";
import { removeDuplicates } from "~/utils/arrays";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
TEAM_SEARCH_PAGE,
@@ -131,7 +131,7 @@ function TeamBanner() {
</div>
) : null}
<div className="team__banner__flags">
{removeDuplicates(
{R.unique(
team.members
.map((member) => member.country)
.filter((country) => country !== null),
@@ -154,7 +154,7 @@ function MobileTeamNameCountry() {
return (
<div className="team__mobile-name-country">
<div className="stack horizontal sm">
{removeDuplicates(
{R.unique(
team.members
.map((member) => member.country)
.filter((country) => country !== null),

View File

@@ -1,5 +1,5 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { removeDuplicates } from "~/utils/arrays";
import * as R from "remeda";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { idObject } from "~/utils/zod";
import { findPlacementsByPlayerId } from "../queries/findPlacements.server";
@@ -13,7 +13,7 @@ export const loader = async (args: LoaderFunctionArgs) => {
const placements = notFoundIfFalsy(findPlacementsByPlayerId(params.id));
const primaryName = placements[0].name;
const aliases = removeDuplicates(
const aliases = R.unique(
placements
.map((placement) => placement.name)
.filter((name) => name !== primaryName),

View File

@@ -1,5 +1,5 @@
import * as R from "remeda";
import { describe, expect, it } from "vitest";
import { removeDuplicates } from "../../../utils/arrays";
import invariant from "../../../utils/invariant";
import { Tournament } from "./Tournament";
import { PADDLING_POOL_255 } from "./tests/mocks";
@@ -47,9 +47,7 @@ describe("round robin standings", () => {
const standings = tournamentPP255.bracketByIdx(0)!.standings;
const groupIds = removeDuplicates(
standings.map((standing) => standing.groupId),
);
const groupIds = R.unique(standings.map((standing) => standing.groupId));
expect(
groupIds.length,
"Paddling Pool 255 should have groups from Group A to Group I",
@@ -81,7 +79,7 @@ describe("round robin standings", () => {
const standings = tournamentPP255.bracketByIdx(0)!.standings;
const placements = removeDuplicates(
const placements = R.unique(
standings.map((standing) => standing.placement),
).sort((a, b) => a - b);

View File

@@ -1,9 +1,9 @@
import { sub } from "date-fns";
import * as R from "remeda";
import type { Tables, TournamentStageSettings } from "~/db/tables";
import { TOURNAMENT } from "~/features/tournament";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import type { Round } from "~/modules/brackets-model";
import { removeDuplicates } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import { assertUnreachable } from "~/utils/types";
@@ -259,7 +259,7 @@ export abstract class Bracket {
}
get participantTournamentTeamIds() {
return removeDuplicates(
return R.unique(
this.data.match
.flatMap((match) => [match.opponent1?.id, match.opponent2?.id])
.filter(Boolean),
@@ -472,7 +472,7 @@ class SingleEliminationBracket extends Bracket {
}
private hasThirdPlaceMatch() {
return removeDuplicates(this.data.match.map((m) => m.group_id)).length > 1;
return R.unique(this.data.match.map((m) => m.group_id)).length > 1;
}
get standings(): Standing[] {
@@ -511,7 +511,7 @@ class SingleEliminationBracket extends Bracket {
this.participantTournamentTeamIds.length - teams.length;
const result: Standing[] = [];
for (const roundId of removeDuplicates(teams.map((team) => team.lostAt))) {
for (const roundId of R.unique(teams.map((team) => team.lostAt))) {
const teamsLostThisRound: { id: number }[] = [];
while (teams.length && teams[0].lostAt === roundId) {
teamsLostThisRound.push(teams.shift()!);
@@ -669,7 +669,7 @@ class DoubleEliminationBracket extends Bracket {
this.participantTournamentTeamIds.length - teams.length;
const result: Standing[] = [];
for (const roundId of removeDuplicates(teams.map((team) => team.lostAt))) {
for (const roundId of R.unique(teams.map((team) => team.lostAt))) {
const teamsLostThisRound: { id: number }[] = [];
while (teams.length && teams[0].lostAt === roundId) {
teamsLostThisRound.push(teams.shift()!);
@@ -892,9 +892,7 @@ class RoundRobinBracket extends Bracket {
const relevantMatchesFinished =
standings.length === this.participantTournamentTeamIds.length;
const uniquePlacements = removeDuplicates(
standings.map((s) => s.placement),
);
const uniquePlacements = R.unique(standings.map((s) => s.placement));
// 1,3,5 -> 1,2,3 e.g.
const placementNormalized = (p: number) => {
@@ -1182,9 +1180,7 @@ class SwissBracket extends Bracket {
});
});
const uniquePlacements = removeDuplicates(
standings.map((s) => s.placement),
);
const uniquePlacements = R.unique(standings.map((s) => s.placement));
// 1,3,5 -> 1,2,3 e.g.
const placementNormalized = (p: number) => {

View File

@@ -1,3 +1,4 @@
import * as R from "remeda";
import type { TournamentRoundMaps } from "~/db/tables";
import type {
ModeShort,
@@ -5,7 +6,6 @@ import type {
StageId,
} from "~/modules/in-game-lists";
import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator";
import { removeDuplicates } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import { assertUnreachable } from "~/utils/types";
@@ -132,7 +132,7 @@ export function mapsListWithLegality(args: MapListWithStatusesArgs) {
}
})();
const modesIncluded = removeDuplicates(mapPool.map((m) => m.mode));
const modesIncluded = R.unique(mapPool.map((m) => m.mode));
const unavailableStagesSet = unavailableStages(args);
const unavailableModesSetAll = unavailableModes(args);

View File

@@ -1,6 +1,6 @@
import compare from "just-compare";
import * as R from "remeda";
import type { PreparedMaps } from "~/db/tables";
import { nullFilledArray, removeDuplicates } from "~/utils/arrays";
import { nullFilledArray } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import type { Bracket } from "./Bracket";
import type { Tournament } from "./Tournament";
@@ -32,11 +32,11 @@ export function resolvePreparedForTheBracket({
] of tournament.ctx.settings.bracketProgression.entries()) {
if (
bracket.type === bracketPreparingFor.type &&
compare(
R.isDeepEqual(
bracket.sources?.map((s) => s.bracketIdx),
bracketPreparingFor.sources?.map((s) => s.bracketIdx),
) &&
compare(bracket.settings, bracketPreparingFor.settings)
R.isDeepEqual(bracket.settings, bracketPreparingFor.settings)
) {
const bracketMaps = preparedByBracket?.[anotherBracketIdx];
@@ -121,7 +121,7 @@ function trimMapsByTeamCount({
nullFilledArray(teamCount).map((_, i) => i + 1),
).round;
const groupIds = removeDuplicates(preparedMaps.maps.map((r) => r.groupId));
const groupIds = R.unique(preparedMaps.maps.map((r) => r.groupId));
const result = { ...preparedMaps };
for (const groupId of groupIds) {
@@ -179,7 +179,7 @@ function thirdPlaceMatchDisappeared({
}
const preparedHasThirdPlace =
removeDuplicates(preparedMaps.maps.map((r) => r.groupId)).length > 1;
R.unique(preparedMaps.maps.map((r) => r.groupId)).length > 1;
return preparedHasThirdPlace && teamCount < 4;
}

View File

@@ -1,6 +1,4 @@
// todo
import compare from "just-compare";
import * as R from "remeda";
import type { Tables, TournamentStageSettings } from "~/db/tables";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import {
@@ -582,7 +580,7 @@ export function changedBracketProgression(
const oldBracket = oldProgression[i];
const newBracket = newProgression.at(i);
if (!newBracket || !compare(oldBracket, newBracket)) {
if (!newBracket || !R.isDeepEqual(oldBracket, newBracket)) {
changed.push(i);
}
}
@@ -604,7 +602,7 @@ export function changedBracketProgressionFormat(
!newBracket ||
newBracket.name !== oldBracket.name ||
newBracket.type !== oldBracket.type ||
!compare(newBracket.settings, oldBracket.settings)
!R.isDeepEqual(newBracket.settings, oldBracket.settings)
) {
return true;
}

View File

@@ -1,3 +1,4 @@
import * as R from "remeda";
import type {
Tables,
TournamentStage,
@@ -13,7 +14,6 @@ import type { Match, Stage } from "~/modules/brackets-model";
import type { ModeShort } from "~/modules/in-game-lists";
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
import { isAdmin } from "~/permissions";
import { removeDuplicates } from "~/utils/arrays";
import {
databaseTimestampNow,
databaseTimestampToDate,
@@ -663,7 +663,7 @@ export class Tournament {
return ["CB"];
}
default: {
const pickedModes = removeDuplicates(
const pickedModes = R.unique(
this.ctx.toSetMapPool.map((map) => map.mode),
);
if (pickedModes.length === 0) {

View File

@@ -1,5 +1,5 @@
import * as R from "remeda";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import { removeDuplicates } from "~/utils/arrays";
import { TOURNAMENT } from "../../tournament/tournament-constants";
export function getRounds(args: {
@@ -52,7 +52,7 @@ export function getRounds(args: {
const hasThirdPlaceMatch =
args.type === "single" &&
removeDuplicates(args.bracketData.match.map((m) => m.group_id)).length > 1;
R.unique(args.bracketData.match.map((m) => m.group_id)).length > 1;
const namedRounds = rounds.map((round, i) => {
const name = () => {
if (

View File

@@ -1,12 +1,11 @@
import shuffle from "just-shuffle";
import type { Rating } from "node_modules/openskill/dist/types";
import { ordinal } from "openskill";
import * as R from "remeda";
import {
identifierToUserIds,
rate,
userIdsToIdentifier,
} from "~/features/mmr/mmr-utils";
import { removeDuplicates } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import type { Tables } from "../../../db/tables";
import type { AllMatchResult } from "../queries/allMatchResultsByTournamentId.server";
@@ -134,12 +133,12 @@ export function calculateIndividualPlayerSkills({
: match.opponentTwo.id;
const participants = match.maps.flatMap((m) => m.participants);
const winnerUserIds = removeDuplicates(
const winnerUserIds = R.unique(
participants
.filter((p) => p.tournamentTeamId === winnerTeamId)
.map((p) => p.userId),
);
const loserUserIds = removeDuplicates(
const loserUserIds = R.unique(
participants
.filter((p) => p.tournamentTeamId !== winnerTeamId)
.map((p) => p.userId),
@@ -282,7 +281,7 @@ function selectMostPopular<T>(items: T[]): T {
return mostPopularItems[0][0];
}
return shuffle(mostPopularItems)[0][0];
return R.shuffle(mostPopularItems)[0][0];
}
function mapResultDeltas(

View File

@@ -1,5 +1,5 @@
import * as R from "remeda";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import { removeDuplicates } from "~/utils/arrays";
import type * as Progression from "../Progression";
import { Tournament } from "../Tournament";
import type { TournamentData } from "../Tournament.server";
@@ -49,11 +49,12 @@ export const testTournament = ({
data?: TournamentManagerDataSet;
ctx?: Partial<TournamentData["ctx"]>;
}) => {
const participant = removeDuplicates(
data.match
.flatMap((m) => [m.opponent1?.id, m.opponent2?.id])
.filter(Boolean),
) as number[];
const participant = R.pipe(
data.match,
R.flatMap((m) => [m.opponent1?.id, m.opponent2?.id]),
R.filter(R.isTruthy),
R.unique<number[]>,
);
return new Tournament({
data,

View File

@@ -1,11 +1,10 @@
/** Map list generation logic for "TO pick" as in the map list is defined beforehand by TO and teams don't pick */
import shuffle from "just-shuffle";
import * as R from "remeda";
import type { Tables, TournamentRoundMaps } from "~/db/tables";
import type { Round } from "~/modules/brackets-model";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import { SENDOUQ_DEFAULT_MAPS } from "~/modules/tournament-map-list-generator/constants";
import { removeDuplicates } from "~/utils/arrays";
import { logger } from "~/utils/logger";
import { assertUnreachable } from "~/utils/types";
@@ -159,8 +158,8 @@ function modeOrder({
iteration: number;
flavor: GenerateTournamentRoundMaplistArgs["flavor"];
}) {
const modes = removeDuplicates(pool.map((x) => x.mode));
const shuffledModes = shuffle(modes);
const modes = R.unique(pool.map((x) => x.mode));
const shuffledModes = R.shuffle(modes);
shuffledModes.sort((a, b) => {
const aFreq = modeFrequency.get(a) ?? 0;
const bFreq = modeFrequency.get(b) ?? 0;
@@ -265,9 +264,9 @@ function resolveStage(
}
}
const stage = shuffle(equallyGoodOptionsIgnoringCombo)[0];
const stage = R.shuffle(equallyGoodOptionsIgnoringCombo)[0];
if (typeof stage !== "number") {
const fallback = shuffle(SENDOUQ_DEFAULT_MAPS[mode].slice())[0];
const fallback = R.shuffle(SENDOUQ_DEFAULT_MAPS[mode])[0];
logger.warn(
`No stage found for mode ${mode} iteration ${currentIteration}, using fallback ${fallback}`,
);

View File

@@ -1,4 +1,5 @@
import type { TFunction } from "i18next";
import * as R from "remeda";
import type { Tables, TournamentRoundMaps } from "~/db/tables";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
@@ -7,8 +8,6 @@ import {
seededRandom,
sourceTypes,
} from "~/modules/tournament-map-list-generator";
import { removeDuplicates } from "~/utils/arrays";
import { sumArray } from "~/utils/number";
import type { FindMatchById } from "../tournament-bracket/queries/findMatchById.server";
import type { TournamentLoaderData } from "../tournament/loaders/to.$id.server";
import type { Standing } from "./core/Bracket";
@@ -110,7 +109,7 @@ export function everyMatchIsOver(
) {
// winners, losers & grand finals+bracket reset are all different stages
const isDoubleElimination =
removeDuplicates(bracket.match.map((match) => match.group_id)).length === 3;
R.unique(bracket.match.map((match) => match.group_id)).length === 3;
// tournament didn't start yet
if (bracket.match.length === 0) return false;
@@ -241,7 +240,7 @@ export function isSetOverByResults({
}
if (countType === "PLAY_ALL") {
return sumArray(Array.from(winCounts.values())) === count;
return R.sum(Array.from(winCounts.values())) === count;
}
const maxWins = Math.max(...Array.from(winCounts.values()));
@@ -260,7 +259,7 @@ export function isSetOverByScore({
countType: TournamentRoundMaps["type"];
}) {
if (countType === "PLAY_ALL") {
return sumArray(scores) === count;
return R.sum(scores) === count;
}
const matchOverAtXWins = Math.ceil(count / 2);

View File

@@ -1,7 +1,7 @@
import * as R from "remeda";
import type { Standing } from "~/features/tournament-bracket/core/Bracket";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import type { Tournament } from "~/features/tournament-bracket/core/Tournament";
import { removeDuplicates } from "~/utils/arrays";
/** Calculates SPR (Seed Performance Rating) - see https://www.pgstats.com/articles/introducing-spr-and-uf */
export function calculateSPR({
@@ -11,7 +11,7 @@ export function calculateSPR({
standings: Standing[];
teamId: number;
}) {
const uniquePlacements = removeDuplicates(
const uniquePlacements = R.unique(
standings.map((standing) => standing.placement),
).sort((a, b) => a - b);

View File

@@ -1,7 +1,7 @@
import * as R from "remeda";
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import { removeDuplicatesByProperty } from "~/utils/arrays";
import { parseDBArray } from "~/utils/sql";
const stm = sql.prepare(/* sql */ `
@@ -111,10 +111,7 @@ export function setHistoryByTeamId(
...row,
matches: parseDBArray(row.matches),
// TODO: there is probably a way to do this in SQL
players: removeDuplicatesByProperty(
parseDBArray(row.players),
(u: Pick<Tables["User"], "id">) => u.id,
),
players: R.uniqueBy(parseDBArray(row.players), (u) => u.id),
};
});
}

View File

@@ -36,7 +36,6 @@ import { useAutoRerender } from "~/hooks/useAutoRerender";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
import { filterOutFalsy } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import {
LOG_IN_URL,
@@ -403,10 +402,7 @@ function RegistrationProgress({
const completedIfTruthy = (condition: unknown) =>
condition ? "completed" : "incomplete";
const steps: Array<{
name: string;
status: "completed" | "incomplete" | "notice";
}> = filterOutFalsy([
const steps = [
{
name: t("tournament:pre.steps.name"),
status: completedIfTruthy(name),
@@ -432,10 +428,10 @@ function RegistrationProgress({
tournament.isLeagueSignup
? {
name: "Google Sheet",
status: "notice",
status: "notice" as const,
}
: null,
]);
].filter((step) => step !== null);
const regClosesBeforeStart =
tournament.registrationClosesAt.getTime() !==

View File

@@ -1,4 +1,5 @@
import { type ActionFunction, redirect } from "@remix-run/node";
import * as R from "remeda";
import { z } from "zod";
import { BUILD } from "~/constants";
import { requireUser } from "~/features/auth/core/user.server";
@@ -15,7 +16,6 @@ import type {
BuildAbilitiesTuple,
MainWeaponId,
} from "~/modules/in-game-lists/types";
import { removeDuplicates } from "~/utils/arrays";
import { unJsonify } from "~/utils/kysely.server";
import { logger } from "~/utils/logger";
import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server";
@@ -194,7 +194,7 @@ function refreshCache({
...oldBuildWeapons.map(({ weaponSplId }) => weaponSplId),
];
const dedupedWeaponSplIds = removeDuplicates(allWeaponSplIds);
const dedupedWeaponSplIds = R.unique(allWeaponSplIds);
refreshBuildsCacheByWeaponSplIds(dedupedWeaponSplIds);
}

View File

@@ -1,7 +1,7 @@
import * as R from "remeda";
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
import type { MainWeaponId, ModeShort, StageId } from "~/modules/in-game-lists";
import { removeDuplicates } from "~/utils/arrays";
import { parseDBArray, parseDBJsonArray } from "~/utils/sql";
import { weaponIdToArrayWithAlts } from "../../../modules/in-game-lists/weapon-ids";
import { VODS_PAGE_BATCH_SIZE } from "../vods-constants";
@@ -87,7 +87,7 @@ export function findVods({
return {
...vod,
weapons: removeDuplicates(parseDBArray(vod.weapons)),
weapons: R.unique(parseDBArray(vod.weapons)),
pov: playerNames[0] ?? players[0],
};
})

View File

@@ -1,9 +1,6 @@
// TODO: when more examples of permissions profile difference between
// this implementation and one that takes arrays
import shuffle from "just-shuffle";
import invariant from "~/utils/invariant";
// (not all arrays need to necessarily run but they need to be defined)
export function allTruthy(arr: unknown[]) {
return arr.every(Boolean);
@@ -55,48 +52,10 @@ export function isDefined<T>(value: T | undefined | null): value is T {
return value !== null && value !== undefined;
}
export function removeDuplicates<T>(arr: T[]): T[] {
const seen = new Set<T>();
return arr.filter((item) => {
if (seen.has(item)) return false;
seen.add(item);
return true;
});
}
export function removeDuplicatesByProperty<T>(
arr: T[],
getter: (arg0: T) => number | string,
): T[] {
const seen = new Set();
return arr.filter((item) => {
const id = getter(item);
if (seen.has(id)) return false;
seen.add(id);
return true;
});
}
export function nullFilledArray(size: number): null[] {
return new Array(size).fill(null);
}
export function pickRandomItem<T>(array: T[]): T {
invariant(array.length > 0, "Can't pick from empty array");
const shuffled = shuffle(structuredClone(array));
return shuffled[0];
}
export function filterOutFalsy<T>(arr: (T | null | undefined)[]): T[] {
return arr.filter(Boolean) as T[];
}
/**
* Calculates the average of an array of numbers. If the array is empty, returns null.
*

View File

@@ -1,3 +1,5 @@
import * as R from "remeda";
export function roundToNDecimalPlaces(num: number, n = 2) {
return Number((Math.round(num * 10 ** n) / 10 ** n).toFixed(n));
}
@@ -9,12 +11,8 @@ export function cutToNDecimalPlaces(num: number, n = 2) {
return Number(n > 0 ? result.replace(/\.?0+$/, "") : result);
}
export function sumArray(arr: number[]) {
return arr.reduce((acc, curr) => acc + curr, 0);
}
export function averageArray(arr: number[]) {
return sumArray(arr) / arr.length;
return R.sum(arr) / arr.length;
}
export function safeNumberParse(value: string | null) {

View File

@@ -5,10 +5,6 @@ export function inGameNameWithoutDiscriminator(inGameName: string) {
return inGameName.split("#")[0];
}
export function semiRandomId() {
return String(Math.random());
}
export const rawSensToString = (sens: number) =>
`${sens > 0 ? "+" : ""}${sens / 10}`;
@@ -53,10 +49,6 @@ export function gearTypeToInitial(gearType: GearType) {
}
}
export function capitalize(str: string) {
return str[0].toUpperCase() + str.slice(1);
}
export function pathnameFromPotentialURL(maybeUrl: string) {
try {
return new URL(maybeUrl).pathname.replace("/", "");

44
package-lock.json generated
View File

@@ -33,10 +33,6 @@
"i18next-browser-languagedetector": "^8.0.4",
"i18next-http-backend": "^2.6.2",
"isbot": "^5.1.25",
"just-capitalize": "^3.2.0",
"just-compare": "^2.3.0",
"just-random-integer": "^4.2.0",
"just-shuffle": "^4.2.0",
"kysely": "^0.27.6",
"lru-cache": "^11.1.0",
"markdown-to-jsx": "^7.7.4",
@@ -56,6 +52,7 @@
"react-i18next": "^15.4.1",
"react-use": "^17.6.0",
"react-use-draggable-scroll": "^0.4.7",
"remeda": "^2.21.2",
"remix-auth": "^4.1.0",
"remix-auth-oauth2": "^3.4.0",
"remix-i18next": "^6.4.1",
@@ -11131,30 +11128,6 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/just-capitalize": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/just-capitalize/-/just-capitalize-3.2.0.tgz",
"integrity": "sha512-FK8U9A5AHCIGxlEXg3RFJkb9Nz/fS9luJlrfRf0bFBZU6xnIQ6tbwl+HitMJLwCFszZqVaXQcyeoy8/PYABS6A==",
"license": "MIT"
},
"node_modules/just-compare": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz",
"integrity": "sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==",
"license": "MIT"
},
"node_modules/just-random-integer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/just-random-integer/-/just-random-integer-4.2.0.tgz",
"integrity": "sha512-MfabwcY+RQNCVCmZZkTYDpk/AT315+7Rkoj59+abzYxgUOiSdoZ4G5hbK/4VZqdHScu95Wnd+1MqxyUETM6xuQ==",
"license": "MIT"
},
"node_modules/just-shuffle": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/just-shuffle/-/just-shuffle-4.2.0.tgz",
"integrity": "sha512-/dDmNseAWLf3XkFY9xf3/BdQoiy27LNUy/7uG4zdSAX526nIHMYPYeJ4pN4lT1/pgNEX8XCXPtUB6gJqTpBEng==",
"license": "MIT"
},
"node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
@@ -14695,6 +14668,15 @@
"integrity": "sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==",
"license": "MIT"
},
"node_modules/remeda": {
"version": "2.21.2",
"resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.2.tgz",
"integrity": "sha512-wdhkMDou8HRpD7RnxKJ/FHJWEGXRH7jV/pb0NsdLLSoBo+G9RjtxcY41hVhogLfEMkThk6aySKjs+Yd6PnpzBA==",
"license": "MIT",
"dependencies": {
"type-fest": "^4.37.0"
}
},
"node_modules/remix-auth": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-4.1.0.tgz",
@@ -16212,9 +16194,9 @@
"license": "ISC"
},
"node_modules/type-fest": {
"version": "4.36.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.36.0.tgz",
"integrity": "sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==",
"version": "4.39.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz",
"integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"

View File

@@ -49,10 +49,6 @@
"i18next-browser-languagedetector": "^8.0.4",
"i18next-http-backend": "^2.6.2",
"isbot": "^5.1.25",
"just-capitalize": "^3.2.0",
"just-compare": "^2.3.0",
"just-random-integer": "^4.2.0",
"just-shuffle": "^4.2.0",
"kysely": "^0.27.6",
"lru-cache": "^11.1.0",
"markdown-to-jsx": "^7.7.4",
@@ -72,6 +68,7 @@
"react-i18next": "^15.4.1",
"react-use": "^17.6.0",
"react-use-draggable-scroll": "^0.4.7",
"remeda": "^2.21.2",
"remix-auth": "^4.1.0",
"remix-auth-oauth2": "^3.4.0",
"remix-i18next": "^6.4.1",