From 994cb973466126d985df0e2d9abfbc712b51f706 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:14:11 +0300 Subject: [PATCH] Take Remeda in use replacing just-utils and own utils --- app/db/seed/index.ts | 71 ++++++++++--------- app/features/build-analyzer/core/stats.ts | 41 +++++------ .../build-analyzer/routes/analyzer.tsx | 5 +- .../calendar/CalendarRepository.server.ts | 4 +- app/features/lfg/components/LFGFilters.tsx | 4 +- .../core/map-list-generator/modes.ts | 6 +- .../map-planner/components/Planner.tsx | 6 +- .../core/objectDamage.ts | 6 +- .../PlusVotingRepository.server.ts | 4 +- .../sendouq-match/core/match.server.ts | 8 +-- app/features/team/loaders/t.server.ts | 4 +- app/features/team/routes/t.$customUrl.tsx | 6 +- .../loaders/xsearch.player.$id.server.ts | 4 +- .../tournament-bracket/core/Bracket.test.ts | 8 +-- .../tournament-bracket/core/Bracket.ts | 18 ++--- .../tournament-bracket/core/PickBan.ts | 4 +- .../tournament-bracket/core/PreparedMaps.ts | 12 ++-- .../tournament-bracket/core/Progression.ts | 8 +-- .../tournament-bracket/core/Tournament.ts | 4 +- .../tournament-bracket/core/rounds.ts | 4 +- .../core/summarizer.server.ts | 9 ++- .../core/tests/test-utils.ts | 13 ++-- .../tournament-bracket/core/toMapList.ts | 11 ++- .../tournament-bracket-utils.ts | 9 ++- app/features/tournament/core/Standings.ts | 4 +- .../queries/setHistoryByTeamId.server.ts | 7 +- .../tournament/routes/to.$id.register.tsx | 10 +-- .../u.$identifier.builds.new.server.ts | 4 +- app/features/vods/queries/findVods.server.ts | 4 +- app/utils/arrays.ts | 41 ----------- app/utils/number.ts | 8 +-- app/utils/strings.ts | 8 --- package-lock.json | 44 ++++-------- package.json | 5 +- 34 files changed, 156 insertions(+), 248 deletions(-) diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index fa02e2b57..01b107682 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -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]; }; diff --git a/app/features/build-analyzer/core/stats.ts b/app/features/build-analyzer/core/stats.ts index 02944c6ae..40e756661 100644 --- a/app/features/build-analyzer/core/stats.ts +++ b/app/features/build-analyzer/core/stats.ts @@ -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({ diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx index a9ffd7247..6c0cfdb9b 100644 --- a/app/features/build-analyzer/routes/analyzer.tsx +++ b/app/features/build-analyzer/routes/analyzer.tsx @@ -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) diff --git a/app/features/calendar/CalendarRepository.server.ts b/app/features/calendar/CalendarRepository.server.ts index 31c49d185..7da01b76e 100644 --- a/app/features/calendar/CalendarRepository.server.ts +++ b/app/features/calendar/CalendarRepository.server.ts @@ -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)), }; } diff --git a/app/features/lfg/components/LFGFilters.tsx b/app/features/lfg/components/LFGFilters.tsx index 68f2f2ca3..e62d69af7 100644 --- a/app/features/lfg/components/LFGFilters.tsx +++ b/app/features/lfg/components/LFGFilters.tsx @@ -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) => ( ))} diff --git a/app/features/map-list-generator/core/map-list-generator/modes.ts b/app/features/map-list-generator/core/map-list-generator/modes.ts index 8a54c22f4..b29cbb64e 100644 --- a/app/features/map-list-generator/core/map-list-generator/modes.ts +++ b/app/features/map-list-generator/core/map-list-generator/modes.ts @@ -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"]); } diff --git a/app/features/map-planner/components/Planner.tsx b/app/features/map-planner/components/Planner.tsx index 83cc4f874..906f742bd 100644 --- a/app/features/map-planner/components/Planner.tsx +++ b/app/features/map-planner/components/Planner.tsx @@ -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"), }); diff --git a/app/features/object-damage-calculator/core/objectDamage.ts b/app/features/object-damage-calculator/core/objectDamage.ts index 8acdbb636..8ab3ce56e 100644 --- a/app/features/object-damage-calculator/core/objectDamage.ts +++ b/app/features/object-damage-calculator/core/objectDamage.ts @@ -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({ diff --git a/app/features/plus-voting/PlusVotingRepository.server.ts b/app/features/plus-voting/PlusVotingRepository.server.ts index 18fb4408a..021de7cce 100644 --- a/app/features/plus-voting/PlusVotingRepository.server.ts +++ b/app/features/plus-voting/PlusVotingRepository.server.ts @@ -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: { diff --git a/app/features/sendouq-match/core/match.server.ts b/app/features/sendouq-match/core/match.server.ts index f1529680c..ace245127 100644 --- a/app/features/sendouq-match/core/match.server.ts +++ b/app/features/sendouq-match/core/match.server.ts @@ -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; diff --git a/app/features/team/loaders/t.server.ts b/app/features/team/loaders/t.server.ts index c80313d64..8265efd92 100644 --- a/app/features/team/loaders/t.server.ts +++ b/app/features/team/loaders/t.server.ts @@ -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[], ) => { - return sumArray( + return R.sum( members .map((m) => m.plusTier ?? 100) .sort((a, b) => a - b) diff --git a/app/features/team/routes/t.$customUrl.tsx b/app/features/team/routes/t.$customUrl.tsx index faaee848d..25fe6f966 100644 --- a/app/features/team/routes/t.$customUrl.tsx +++ b/app/features/team/routes/t.$customUrl.tsx @@ -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() { ) : null}
- {removeDuplicates( + {R.unique( team.members .map((member) => member.country) .filter((country) => country !== null), @@ -154,7 +154,7 @@ function MobileTeamNameCountry() { return (
- {removeDuplicates( + {R.unique( team.members .map((member) => member.country) .filter((country) => country !== null), diff --git a/app/features/top-search/loaders/xsearch.player.$id.server.ts b/app/features/top-search/loaders/xsearch.player.$id.server.ts index 50897bc00..19e9cb781 100644 --- a/app/features/top-search/loaders/xsearch.player.$id.server.ts +++ b/app/features/top-search/loaders/xsearch.player.$id.server.ts @@ -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), diff --git a/app/features/tournament-bracket/core/Bracket.test.ts b/app/features/tournament-bracket/core/Bracket.test.ts index e5cb4af05..795a067e4 100644 --- a/app/features/tournament-bracket/core/Bracket.test.ts +++ b/app/features/tournament-bracket/core/Bracket.test.ts @@ -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); diff --git a/app/features/tournament-bracket/core/Bracket.ts b/app/features/tournament-bracket/core/Bracket.ts index 627ced2e6..0c1e62a65 100644 --- a/app/features/tournament-bracket/core/Bracket.ts +++ b/app/features/tournament-bracket/core/Bracket.ts @@ -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) => { diff --git a/app/features/tournament-bracket/core/PickBan.ts b/app/features/tournament-bracket/core/PickBan.ts index 85d8c16df..325d89375 100644 --- a/app/features/tournament-bracket/core/PickBan.ts +++ b/app/features/tournament-bracket/core/PickBan.ts @@ -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); diff --git a/app/features/tournament-bracket/core/PreparedMaps.ts b/app/features/tournament-bracket/core/PreparedMaps.ts index a358b09a4..c4fff984a 100644 --- a/app/features/tournament-bracket/core/PreparedMaps.ts +++ b/app/features/tournament-bracket/core/PreparedMaps.ts @@ -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; } diff --git a/app/features/tournament-bracket/core/Progression.ts b/app/features/tournament-bracket/core/Progression.ts index ee1b9af63..d7afe12ac 100644 --- a/app/features/tournament-bracket/core/Progression.ts +++ b/app/features/tournament-bracket/core/Progression.ts @@ -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; } diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts index 4ec17df89..545836afc 100644 --- a/app/features/tournament-bracket/core/Tournament.ts +++ b/app/features/tournament-bracket/core/Tournament.ts @@ -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) { diff --git a/app/features/tournament-bracket/core/rounds.ts b/app/features/tournament-bracket/core/rounds.ts index 7d5416769..f368c1d94 100644 --- a/app/features/tournament-bracket/core/rounds.ts +++ b/app/features/tournament-bracket/core/rounds.ts @@ -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 ( diff --git a/app/features/tournament-bracket/core/summarizer.server.ts b/app/features/tournament-bracket/core/summarizer.server.ts index 1675fdf56..421da6373 100644 --- a/app/features/tournament-bracket/core/summarizer.server.ts +++ b/app/features/tournament-bracket/core/summarizer.server.ts @@ -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(items: T[]): T { return mostPopularItems[0][0]; } - return shuffle(mostPopularItems)[0][0]; + return R.shuffle(mostPopularItems)[0][0]; } function mapResultDeltas( diff --git a/app/features/tournament-bracket/core/tests/test-utils.ts b/app/features/tournament-bracket/core/tests/test-utils.ts index 6693c8b34..8518b928e 100644 --- a/app/features/tournament-bracket/core/tests/test-utils.ts +++ b/app/features/tournament-bracket/core/tests/test-utils.ts @@ -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; }) => { - 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, + ); return new Tournament({ data, diff --git a/app/features/tournament-bracket/core/toMapList.ts b/app/features/tournament-bracket/core/toMapList.ts index a176c78cd..653935568 100644 --- a/app/features/tournament-bracket/core/toMapList.ts +++ b/app/features/tournament-bracket/core/toMapList.ts @@ -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}`, ); diff --git a/app/features/tournament-bracket/tournament-bracket-utils.ts b/app/features/tournament-bracket/tournament-bracket-utils.ts index b0b507226..350e5c43f 100644 --- a/app/features/tournament-bracket/tournament-bracket-utils.ts +++ b/app/features/tournament-bracket/tournament-bracket-utils.ts @@ -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); diff --git a/app/features/tournament/core/Standings.ts b/app/features/tournament/core/Standings.ts index 99230636f..1c0aed947 100644 --- a/app/features/tournament/core/Standings.ts +++ b/app/features/tournament/core/Standings.ts @@ -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); diff --git a/app/features/tournament/queries/setHistoryByTeamId.server.ts b/app/features/tournament/queries/setHistoryByTeamId.server.ts index 1fbbff89a..65792375f 100644 --- a/app/features/tournament/queries/setHistoryByTeamId.server.ts +++ b/app/features/tournament/queries/setHistoryByTeamId.server.ts @@ -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) => u.id, - ), + players: R.uniqueBy(parseDBArray(row.players), (u) => u.id), }; }); } diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index 7d17d4881..1526cea8e 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -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() !== diff --git a/app/features/user-page/actions/u.$identifier.builds.new.server.ts b/app/features/user-page/actions/u.$identifier.builds.new.server.ts index 2a34358a1..73562e17e 100644 --- a/app/features/user-page/actions/u.$identifier.builds.new.server.ts +++ b/app/features/user-page/actions/u.$identifier.builds.new.server.ts @@ -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); } diff --git a/app/features/vods/queries/findVods.server.ts b/app/features/vods/queries/findVods.server.ts index 4a009db6b..811c3ed3a 100644 --- a/app/features/vods/queries/findVods.server.ts +++ b/app/features/vods/queries/findVods.server.ts @@ -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], }; }) diff --git a/app/utils/arrays.ts b/app/utils/arrays.ts index 554a4a920..d287ce0cd 100644 --- a/app/utils/arrays.ts +++ b/app/utils/arrays.ts @@ -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(value: T | undefined | null): value is T { return value !== null && value !== undefined; } -export function removeDuplicates(arr: T[]): T[] { - const seen = new Set(); - - return arr.filter((item) => { - if (seen.has(item)) return false; - seen.add(item); - - return true; - }); -} - -export function removeDuplicatesByProperty( - 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(array: T[]): T { - invariant(array.length > 0, "Can't pick from empty array"); - - const shuffled = shuffle(structuredClone(array)); - - return shuffled[0]; -} - -export function filterOutFalsy(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. * diff --git a/app/utils/number.ts b/app/utils/number.ts index 735b8ffc2..7999fa019 100644 --- a/app/utils/number.ts +++ b/app/utils/number.ts @@ -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) { diff --git a/app/utils/strings.ts b/app/utils/strings.ts index 99737663d..4784bbe54 100644 --- a/app/utils/strings.ts +++ b/app/utils/strings.ts @@ -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("/", ""); diff --git a/package-lock.json b/package-lock.json index 72f72fb0a..f84d6f99b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 17a46fec2..7f40a7aa9 100644 --- a/package.json +++ b/package.json @@ -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",