From fd48bced91bc44a4b28f24abd8bff84beaeba045 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:07:17 +0300 Subject: [PATCH] Migrate Prettier/Eslint/Stylelint setup to Biome (#1772) * Initial * CSS lint * Test CI * Add 1v1, 2v2, and 3v3 Tags (#1771) * Initial * CSS lint * Test CI * Rename step --------- Co-authored-by: xi <104683822+ximk@users.noreply.github.com> --- .eslintrc.cjs | 44 - .github/workflows/main.yml | 8 +- .prettierignore | 1 - .stylelintrc.json | 20 - README.md | 2 +- app/components/AbilitiesSelector.tsx | 332 +- app/components/Ability.tsx | 138 +- app/components/Alert.tsx | 76 +- app/components/Avatar.tsx | 92 +- app/components/Badge.tsx | 65 +- app/components/BuildCard.tsx | 514 +- app/components/Button.tsx | 226 +- app/components/Catcher.tsx | 140 +- app/components/Chart.tsx | 260 +- app/components/Combobox.tsx | 718 +- .../ConditionalScrollRestoration.tsx | 28 +- app/components/CustomizedColorsInput.tsx | 116 +- app/components/DateInput.tsx | 118 +- app/components/DetailsSummary.tsx | 20 +- app/components/Dialog.tsx | 161 +- app/components/Divider.tsx | 22 +- app/components/Draggable.tsx | 50 +- app/components/Flag.tsx | 24 +- app/components/FormErrors.tsx | 34 +- app/components/FormMessage.tsx | 32 +- app/components/FormWithConfirm.tsx | 164 +- app/components/FriendCodeInput.tsx | 70 +- app/components/Image.tsx | 266 +- app/components/InfoPopover.tsx | 10 +- app/components/Input.tsx | 144 +- app/components/Label.tsx | 70 +- app/components/Main.tsx | 100 +- app/components/MapPoolSelector.tsx | 776 +- app/components/Menu.tsx | 105 +- app/components/NewTabs.tsx | 240 +- app/components/Pagination.tsx | 84 +- app/components/Placement.tsx | 112 +- app/components/Popover.tsx | 108 +- app/components/Redirect.tsx | 10 +- app/components/RelativeTime.tsx | 44 +- app/components/RequiredHiddenInput.tsx | 34 +- app/components/Section.tsx | 24 +- app/components/SubNav.tsx | 104 +- app/components/SubmitButton.tsx | 64 +- app/components/Table.tsx | 2 +- app/components/Tabs.tsx | 70 +- app/components/Toggle.tsx | 50 +- app/components/UserSearch.tsx | 232 +- app/components/YouTubeEmbed.tsx | 40 +- app/components/icons/Admin.tsx | 28 +- app/components/icons/Alert.tsx | 28 +- app/components/icons/ArchiveBox.tsx | 32 +- app/components/icons/ArrowLeft.tsx | 40 +- app/components/icons/ArrowLongLeft.tsx | 32 +- app/components/icons/ArrowRight.tsx | 38 +- app/components/icons/ArrowsPointingIn.tsx | 32 +- app/components/icons/Battlefy.tsx | 66 +- app/components/icons/Beaker.tsx | 32 +- app/components/icons/BeakerFilled.tsx | 28 +- app/components/icons/Calendar.tsx | 38 +- app/components/icons/ChartBar.tsx | 42 +- app/components/icons/Chat.tsx | 38 +- app/components/icons/CheckIn.tsx | 22 +- app/components/icons/Checkmark.tsx | 44 +- app/components/icons/Clipboard.tsx | 32 +- app/components/icons/Clock.tsx | 28 +- app/components/icons/Cross.tsx | 28 +- app/components/icons/Discord.tsx | 24 +- app/components/icons/Download.tsx | 32 +- app/components/icons/Edit.tsx | 30 +- app/components/icons/Error.tsx | 28 +- app/components/icons/Eye.tsx | 42 +- app/components/icons/EyeSlash.tsx | 32 +- app/components/icons/Filter.tsx | 32 +- app/components/icons/Fire.tsx | 52 +- app/components/icons/GitHub.tsx | 26 +- app/components/icons/Globe.tsx | 56 +- app/components/icons/Heart.tsx | 20 +- app/components/icons/Info.tsx | 28 +- app/components/icons/Link.tsx | 28 +- app/components/icons/Lock.tsx | 28 +- app/components/icons/LogIn.tsx | 44 +- app/components/icons/LogOut.tsx | 32 +- app/components/icons/Map.tsx | 28 +- app/components/icons/Microphone.tsx | 32 +- app/components/icons/MicrophoneFilled.tsx | 22 +- app/components/icons/Minus.tsx | 32 +- app/components/icons/Moon.tsx | 56 +- app/components/icons/Patreon.tsx | 28 +- app/components/icons/Pick.tsx | 32 +- app/components/icons/Plus.tsx | 32 +- app/components/icons/Puzzle.tsx | 20 +- app/components/icons/Refresh.tsx | 32 +- app/components/icons/RefreshArrows.tsx | 32 +- app/components/icons/Scale.tsx | 28 +- app/components/icons/Search.tsx | 32 +- app/components/icons/Speaker.tsx | 32 +- app/components/icons/SpeakerFilled.tsx | 22 +- app/components/icons/SpeakerX.tsx | 32 +- app/components/icons/SpeechBubble.tsx | 32 +- app/components/icons/Star.tsx | 32 +- app/components/icons/StarFilled.tsx | 28 +- app/components/icons/Success.tsx | 28 +- app/components/icons/Sun.tsx | 58 +- app/components/icons/SunAndMoon.tsx | 56 +- app/components/icons/Trash.tsx | 32 +- app/components/icons/Twitch.tsx | 24 +- app/components/icons/Twitter.tsx | 26 +- app/components/icons/Undo.tsx | 28 +- app/components/icons/Unlock.tsx | 20 +- app/components/icons/Upload.tsx | 32 +- app/components/icons/User.tsx | 28 +- app/components/icons/Users.tsx | 20 +- app/components/icons/YouTube.tsx | 24 +- app/components/layout/Footer.tsx | 188 +- app/components/layout/LanguageChanger.tsx | 90 +- .../layout/LogInButtonContainer.tsx | 107 +- app/components/layout/SelectedThemeIcon.tsx | 30 +- app/components/layout/SideNav.tsx | 46 +- app/components/layout/ThemeChanger.tsx | 84 +- app/components/layout/TopRightButtons.tsx | 48 +- app/components/layout/UserItem.tsx | 56 +- app/components/layout/index.tsx | 259 +- app/components/layout/nav-items.json | 324 +- app/components/ramp/Ramp.tsx | 181 +- app/components/ramp/RampUnit.tsx | 116 +- app/components/ramp/store.ts | 20 +- app/constants.ts | 116 +- app/db/seed/index.ts | 2985 +- app/db/seed/placements.json | 120000 +++++++-------- app/db/sql.ts | 32 +- app/db/tables.ts | 1165 +- app/db/types.ts | 754 +- app/entry.client.tsx | 28 +- app/entry.server.tsx | 249 +- app/features/admin/AdminRepository.server.ts | 156 +- app/features/admin/actions/admin.server.ts | 260 +- app/features/admin/core/plus-tier.server.ts | 60 +- app/features/admin/loaders/admin.server.ts | 14 +- app/features/admin/routes/admin.test.ts | 352 +- app/features/admin/routes/admin.tsx | 504 +- app/features/api-private/routes/patrons.tsx | 18 +- app/features/api-private/routes/seed.tsx | 26 +- app/features/api-private/routes/users.tsx | 14 +- .../api-public/api-public-utils.server.ts | 30 +- .../api-public/routes/tournament-match.$id.ts | 276 +- .../routes/tournament.$id.brackets.$bidx.ts | 64 +- .../api-public/routes/tournament.$id.teams.ts | 258 +- .../api-public/routes/tournament.$id.ts | 120 +- .../api-public/routes/user.$identifier.ts | 260 +- app/features/api-public/schema.ts | 324 +- app/features/art/art-constants.ts | 10 +- app/features/art/art-schemas.server.ts | 58 +- app/features/art/art-types.ts | 30 +- app/features/art/art-utils.ts | 8 +- app/features/art/components/ArtGrid.tsx | 398 +- app/features/art/queries/addNewArt.server.ts | 104 +- app/features/art/queries/allArtTags.server.ts | 2 +- .../art/queries/artsByUserId.server.ts | 40 +- .../art/queries/countArtByUserId.server.ts | 2 +- app/features/art/queries/deleteArt.server.ts | 2 +- .../art/queries/findArtById.server.ts | 26 +- app/features/art/queries/makeArtist.server.ts | 2 +- .../art/queries/showcaseArts.server.ts | 62 +- app/features/art/routes/art.new.tsx | 780 +- app/features/art/routes/art.tsx | 198 +- .../articles/articles-schemas.server.ts | 6 +- app/features/articles/core/bySlug.server.ts | 60 +- app/features/articles/core/list.server.ts | 58 +- app/features/articles/routes/a.$slug.tsx | 132 +- app/features/articles/routes/a.tsx | 70 +- .../auth/core/DiscordStrategy.server.ts | 284 +- .../auth/core/authenticator.server.ts | 8 +- app/features/auth/core/routes.server.ts | 202 +- app/features/auth/core/session.server.ts | 24 +- app/features/auth/core/user.server.ts | 54 +- app/features/auth/core/user.ts | 8 +- .../auth/queries/createLogInLink.server.ts | 14 +- .../queries/deleteLogInLinkByCode.server.ts | 2 +- .../queries/userIdByLogInLinkCode.server.ts | 12 +- app/features/badges/BadgeRepository.server.ts | 232 +- app/features/badges/badges-schemas.server.ts | 16 +- .../badges/queries/syncXPBadges.server.ts | 38 +- .../badges/routes/badges.$id.edit.tsx | 496 +- app/features/badges/routes/badges.$id.tsx | 160 +- app/features/badges/routes/badges.tsx | 216 +- app/features/ban/core/banned.server.ts | 18 +- app/features/ban/loaders/suspended.server.ts | 34 +- .../ban/queries/allBannedUsers.server.ts | 14 +- app/features/ban/routes/suspended.tsx | 56 +- .../build-analyzer/analyzer-constants.ts | 210 +- app/features/build-analyzer/analyzer-hooks.ts | 294 +- app/features/build-analyzer/analyzer-types.ts | 468 +- app/features/build-analyzer/analyzer.css | 384 +- .../components/PerInkTankGrid.tsx | 522 +- .../build-analyzer/core/ability-values.ts | 140 +- .../core/abilityChunksCalc.test.ts | 240 +- .../build-analyzer/core/abilityChunksCalc.ts | 146 +- .../core/specialEffects.test.ts | 112 +- .../build-analyzer/core/specialEffects.ts | 302 +- .../build-analyzer/core/stats.test.ts | 178 +- app/features/build-analyzer/core/stats.ts | 2892 +- .../build-analyzer/core/utils.test.ts | 48 +- app/features/build-analyzer/core/utils.ts | 420 +- .../build-analyzer/core/weapon-params.ts | 5686 +- app/features/build-analyzer/index.ts | 34 +- .../build-analyzer/routes/analyzer.tsx | 3100 +- .../build-stats/build-stats-utils.test.ts | 204 +- app/features/build-stats/build-stats-utils.ts | 230 +- app/features/build-stats/build-stats.css | 26 +- .../queries/abilitiesByWeaponId.server.ts | 16 +- .../queries/averageAbilityPoints.server.ts | 16 +- .../routes/builds.$slug.popular.tsx | 211 +- .../build-stats/routes/builds.$slug.stats.tsx | 331 +- app/features/builds/BuildRepository.server.ts | 338 +- app/features/builds/builds-schemas.server.ts | 40 +- app/features/builds/builds-types.ts | 26 +- .../builds/components/FilterSection.tsx | 444 +- .../builds/core/cached-builds.server.ts | 22 +- app/features/builds/core/filter.server.ts | 136 +- app/features/builds/core/filter.test.ts | 452 +- app/features/builds/index.ts | 4 +- .../builds/loaders/builds.$slug.server.ts | 84 +- .../builds/queries/buildsBy.server.ts | 166 +- app/features/builds/routes/builds.$slug.tsx | 566 +- app/features/builds/routes/builds.tsx | 142 +- .../calendar/CalendarRepository.server.ts | 1330 +- .../calendar/actions/calendar.new.server.ts | 514 +- app/features/calendar/calendar-constants.ts | 154 +- app/features/calendar/calendar-types.ts | 4 +- .../calendar/calendar-utils.server.ts | 156 +- app/features/calendar/calendar-utils.ts | 258 +- app/features/calendar/components/Tags.tsx | 100 +- .../calendar/loaders/calendar.new.server.ts | 113 +- .../routes/calendar.$id.report-winners.tsx | 733 +- app/features/calendar/routes/calendar.$id.tsx | 574 +- app/features/calendar/routes/calendar.new.tsx | 2814 +- app/features/calendar/routes/calendar.tsx | 954 +- .../calendar/routes/map-pool-events.ts | 10 +- .../chat/NotificationService.server.ts | 46 +- app/features/chat/chat-hooks.ts | 104 +- app/features/chat/chat-types.ts | 36 +- app/features/chat/chat-utils.ts | 20 +- app/features/chat/components/Chat.tsx | 790 +- app/features/front-page/routes/index.tsx | 500 +- .../front-page/routes/patrons-list.ts | 22 +- .../img-upload/queries/addNewImage.ts | 50 +- .../queries/countAllUnvalidatedImg.server.ts | 2 +- .../queries/countUnvalidatedArt.server.ts | 2 +- .../queries/countUnvalidatedImg.server.ts | 2 +- .../img-upload/queries/oneUnvalidatedImage.ts | 6 +- .../img-upload/queries/validateImage.ts | 2 +- .../img-upload/routes/upload.admin.tsx | 68 +- app/features/img-upload/routes/upload.tsx | 310 +- app/features/img-upload/s3.server.ts | 150 +- app/features/img-upload/upload-constants.ts | 12 +- .../img-upload/upload-schemas.server.ts | 2 +- app/features/img-upload/upload-utils.ts | 4 +- app/features/info/routes/contributions.tsx | 354 +- app/features/info/routes/faq.tsx | 55 +- app/features/info/routes/privacy-policy.tsx | 21 +- app/features/info/routes/support.tsx | 332 +- app/features/info/support.css | 36 +- .../LeaderboardRepository.server.ts | 190 +- .../leaderboards/components/TopTenPlayer.tsx | 130 +- .../leaderboards/core/leaderboards.server.ts | 217 +- .../leaderboards/leaderboards-constants.ts | 26 +- .../leaderboards/leaderboards-utils.ts | 42 +- .../queries/XPLeaderboard.server.ts | 36 +- .../seasonPopularUsersWeapon.server.ts | 32 +- .../queries/userSPLeaderboard.server.ts | 34 +- .../leaderboards/routes/leaderboards.tsx | 924 +- app/features/leaderboards/top-ten.json | 552 +- app/features/lfg/LFGRepository.server.ts | 232 +- app/features/lfg/actions/lfg.new.server.ts | 110 +- app/features/lfg/actions/lfg.server.ts | 58 +- .../lfg/components/LFGAddFilterButton.tsx | 78 +- app/features/lfg/components/LFGFilters.tsx | 494 +- app/features/lfg/components/LFGPost.tsx | 810 +- app/features/lfg/core/filtering.ts | 250 +- app/features/lfg/core/timezone.ts | 36 +- app/features/lfg/lfg-constants.ts | 714 +- app/features/lfg/lfg-types.ts | 224 +- app/features/lfg/lfg.css | 154 +- app/features/lfg/loaders/lfg.new.server.ts | 62 +- app/features/lfg/loaders/lfg.server.ts | 82 +- app/features/lfg/routes/lfg.new.tsx | 444 +- app/features/lfg/routes/lfg.tsx | 242 +- app/features/links/links.json | 190 +- app/features/links/routes/links.tsx | 88 +- .../core/map-list-generator/map-list.ts | 254 +- .../core/map-list-generator/modes.ts | 14 +- .../core/map-list-generator/utils.ts | 36 +- .../map-pool-serializer/serializer.test.ts | 104 +- .../core/map-pool-serializer/serializer.ts | 102 +- .../core/map-pool-serializer/types.ts | 2 +- .../map-list-generator/core/map-pool.ts | 276 +- .../map-list-generator/routes/maps.tsx | 398 +- .../map-planner/components/Planner.tsx | 874 +- app/features/map-planner/plans.css | 6 +- app/features/map-planner/routes/plans.tsx | 48 +- app/features/mmr/mmr-constants.ts | 116 +- app/features/mmr/mmr-utils.server.ts | 72 +- app/features/mmr/mmr-utils.ts | 92 +- .../findCurrentSkillByUserId.server.ts | 16 +- ...findCurrentTeamSkillByIdentifier.server.ts | 16 +- .../mmr/queries/orderedMMRBySeason.server.ts | 16 +- .../queries/seasonAllMMRByUserId.server.ts | 16 +- app/features/mmr/season.ts | 124 +- app/features/mmr/tiered.server.ts | 202 +- .../calculator-constants.ts | 352 +- .../calculator-hooks.ts | 278 +- .../calculator-types.ts | 8 +- .../object-damage-calculator/calculator.css | 178 +- .../core/object-dmg.json | 9030 +- .../core/objectDamage.test.ts | 362 +- .../core/objectDamage.ts | 500 +- .../core/objectHitPoints.ts | 112 +- .../routes/object-damage-calculator.tsx | 672 +- .../PlusSuggestionRepository.server.ts | 220 +- .../plus-suggestions/routes/plus.index.tsx | 2 +- ...plus.suggestions.comment.$tier.$userId.tsx | 184 +- .../routes/plus.suggestions.new.tsx | 430 +- .../routes/plus.suggestions.tsx | 704 +- app/features/plus-suggestions/routes/plus.tsx | 34 +- .../PlusVotingRepository.server.ts | 216 +- app/features/plus-voting/core/index.ts | 6 +- app/features/plus-voting/core/types.ts | 4 +- .../plus-voting/core/usePlusVoting.ts | 278 +- app/features/plus-voting/core/voting-time.ts | 92 +- app/features/plus-voting/routes/plus.list.ts | 26 +- .../routes/plus.voting.results.tsx | 336 +- .../plus-voting/routes/plus.voting.tsx | 512 +- .../sendouq-match/QMatchRepository.server.ts | 308 +- .../components/AddPrivateNoteDialog.tsx | 224 +- .../QSettingsRepository.server.ts | 192 +- app/features/sendouq-settings/banned-maps.ts | 68 +- .../components/ModeMapPoolPicker.tsx | 294 +- .../q-settings-schemas.server.ts | 138 +- app/features/sendouq-settings/q-settings.css | 74 +- .../sendouq-settings/routes/q.settings.tsx | 1403 +- .../QStreamsRepository.server.ts | 50 +- .../sendouq-streams/core/streams.server.ts | 148 +- .../sendouq-streams/routes/q.streams.tsx | 206 +- app/features/sendouq/QRepository.server.ts | 508 +- app/features/sendouq/components/GroupCard.tsx | 1330 +- .../sendouq/components/GroupLeaver.tsx | 68 +- .../sendouq/components/MemberAdder.tsx | 209 +- app/features/sendouq/core/groups.server.ts | 638 +- app/features/sendouq/core/groups.ts | 38 +- .../sendouq/core/match.server.test.ts | 302 +- app/features/sendouq/core/match.server.ts | 552 +- app/features/sendouq/core/match.ts | 28 +- .../core/reported-weapons.server.test.ts | 160 +- .../sendouq/core/reported-weapons.server.ts | 140 +- app/features/sendouq/core/skills.server.ts | 398 +- .../sendouq/core/summarizer.server.ts | 238 +- app/features/sendouq/q-constants.ts | 10 +- app/features/sendouq/q-hooks.ts | 46 +- app/features/sendouq/q-schemas.server.ts | 280 +- app/features/sendouq/q-types.ts | 100 +- app/features/sendouq/q-utils.ts | 152 +- app/features/sendouq/q.css | 614 +- .../sendouq/queries/addDummySkill.server.ts | 2 +- .../sendouq/queries/addInitialSkill.server.ts | 30 +- .../sendouq/queries/addLike.server.ts | 10 +- .../sendouq/queries/addManagerRole.server.ts | 10 +- .../sendouq/queries/addMapResults.server.ts | 12 +- .../sendouq/queries/addMember.server.ts | 14 +- .../queries/addPlayerResults.server.ts | 6 +- .../queries/addReportedWeapons.server.ts | 16 +- .../sendouq/queries/addSkills.server.ts | 102 +- .../queries/chatCodeByGroupId.server.ts | 2 +- .../sendouq/queries/createMatch.server.ts | 64 +- .../sendouq/queries/deleteLike.server.ts | 10 +- .../queries/deleteLikesByGroupId.server.ts | 2 +- .../deleteReportedWeaponsByMatchId.server.ts | 12 +- .../findCurrentGroupByUserId.server.ts | 18 +- .../queries/findGroupByInviteCode.server.ts | 20 +- app/features/sendouq/queries/findLikes.ts | 8 +- .../sendouq/queries/findMatchById.server.ts | 40 +- .../queries/findPreparingGroup.server.ts | 30 +- .../findRecentMatchPlayersByUserId.server.ts | 2 +- .../sendouq/queries/groupHasMatch.server.ts | 2 +- .../sendouq/queries/groupSize.server.ts | 2 +- .../sendouq/queries/groupSuccessorOwner.ts | 18 +- .../sendouq/queries/leaveGroup.server.ts | 44 +- .../sendouq/queries/likeExists.server.ts | 10 +- .../sendouq/queries/morphGroups.server.ts | 68 +- .../sendouq/queries/refreshGroup.server.ts | 2 +- .../queries/removeManagerRole.server.ts | 10 +- .../sendouq/queries/reportScore.server.ts | 44 +- .../reportedWeaponsByMatchId.server.ts | 16 +- .../seasonMapWinrateByUserId.server.ts | 10 +- .../queries/seasonMatchesByUserId.server.ts | 148 +- .../seasonReportedWeaponsByUserId.server.ts | 20 +- .../seasonSetWinrateByUserId.server.ts | 10 +- .../queries/seasonStagesByUserId.server.ts | 58 +- .../seasonsMatesEnemiesByUserId.server.ts | 40 +- .../queries/setGroupAsActive.server.ts | 2 +- .../queries/setGroupAsInactive.server.ts | 2 +- .../sendouq/queries/syncGroupTeamId.server.ts | 24 +- .../sendouq/queries/updateNote.server.ts | 8 +- .../queries/weaponUsageStats.server.ts | 142 +- app/features/sendouq/routes/play.tsx | 2 +- app/features/sendouq/routes/q.info.tsx | 874 +- app/features/sendouq/routes/q.looking.test.ts | 346 +- app/features/sendouq/routes/q.looking.tsx | 1542 +- app/features/sendouq/routes/q.match.$id.tsx | 3204 +- app/features/sendouq/routes/q.preparing.tsx | 234 +- app/features/sendouq/routes/q.rules.tsx | 206 +- app/features/sendouq/routes/q.tsx | 932 +- app/features/sendouq/routes/tiers.tsx | 108 +- app/features/sendouq/routes/trusters.ts | 8 +- app/features/sendouq/routes/weapon-usage.tsx | 24 +- app/features/team/TeamRepository.server.ts | 24 +- app/features/team/actions/t.server.ts | 52 +- app/features/team/loaders/t.server.ts | 88 +- .../team/queries/addNewTeamMember.server.ts | 10 +- app/features/team/queries/allTeams.server.ts | 24 +- .../team/queries/createNewTeam.server.ts | 40 +- .../team/queries/deleteTeam.server.ts | 2 +- app/features/team/queries/edit.server.ts | 28 +- app/features/team/queries/editRole.server.ts | 14 +- .../team/queries/findByIdentifier.server.ts | 98 +- .../team/queries/inviteCodeById.server.ts | 2 +- app/features/team/queries/leaveTeam.server.ts | 10 +- .../team/queries/resetInviteLink.server.ts | 2 +- .../team/queries/transferOwnership.server.ts | 24 +- .../team/routes/t.$customUrl.edit.tsx | 368 +- .../team/routes/t.$customUrl.join.tsx | 190 +- .../team/routes/t.$customUrl.roster.tsx | 444 +- app/features/team/routes/t.$customUrl.tsx | 524 +- app/features/team/routes/t.tsx | 298 +- app/features/team/team-constants.ts | 24 +- app/features/team/team-schemas.server.ts | 66 +- app/features/team/team-types.ts | 44 +- app/features/team/team-utils.ts | 32 +- app/features/team/team.css | 419 +- app/features/theme/core/provider.tsx | 201 +- app/features/theme/core/session.server.ts | 44 +- app/features/theme/routes/theme.ts | 52 +- .../top-search/components/Placements.tsx | 144 +- .../queries/findPlacements.server.ts | 44 +- app/features/top-search/queries/monthYears.ts | 4 +- .../queries/userTopPlacements.server.ts | 42 +- .../top-search/routes/xsearch.player.$id.tsx | 142 +- app/features/top-search/routes/xsearch.tsx | 334 +- app/features/top-search/top-search-utils.ts | 38 +- app/features/top-search/top-search.css | 92 +- .../TournamentMatchRepository.server.ts | 18 +- .../components/Bracket/Elimination.tsx | 160 +- .../components/Bracket/Match.tsx | 410 +- .../components/Bracket/RoundHeader.tsx | 122 +- .../components/Bracket/RoundRobin.tsx | 464 +- .../components/Bracket/Swiss.tsx | 766 +- .../components/Bracket/bracket.css | 200 +- .../components/Bracket/index.tsx | 152 +- .../components/Bracket/useDeadline.ts | 278 +- .../components/BracketMapListDialog.tsx | 1048 +- .../components/CastInfo.tsx | 212 +- .../components/MatchActions.tsx | 616 +- .../components/MatchActionsBanPicker.tsx | 518 +- .../components/MatchRosters.tsx | 274 +- .../OrganizerMatchMapListDialog.tsx | 168 +- .../components/StartedMatch.tsx | 1283 +- .../components/TeamRosterInputs.tsx | 951 +- .../components/TournamentTeamActions.tsx | 302 +- .../tournament-bracket/core/Bracket.ts | 2377 +- .../tournament-bracket/core/PickBan.ts | 312 +- app/features/tournament-bracket/core/Swiss.ts | 840 +- .../core/Tournament.server.ts | 120 +- .../core/Tournament.test.ts | 212 +- .../tournament-bracket/core/Tournament.ts | 2108 +- .../core/brackets-manager/crud-db.server.ts | 602 +- .../core/brackets-manager/crud.server.ts | 517 +- .../core/brackets-manager/manager.server.ts | 12 +- .../core/brackets-manager/manager.ts | 8 +- .../core/emitters.server.ts | 4 +- .../tournament-bracket/core/mapList.server.ts | 398 +- .../tournament-bracket/core/rounds.ts | 148 +- .../core/summarizer.server.ts | 752 +- .../core/summarizer.test.ts | 582 +- .../tournament-bracket/core/tests/mocks.ts | 35904 ++--- .../core/tests/test-utils.ts | 228 +- .../tournament-bracket/core/toMapList.ts | 342 +- .../queries/addSummary.server.ts | 126 +- .../allMatchResultsByTournamentId.server.ts | 62 +- .../queries/bestOfsByTournamentId.server.ts | 6 +- .../deleteMatchPickBanEvents.server.ts | 2 +- ...eParticipantsByMatchGameResultId.server.ts | 4 +- .../queries/deletePickBanEvent.server.ts | 10 +- ...eteTournamentMatchGameResultById.server.ts | 2 +- .../queries/findMapPoolByTeamId.server.ts | 2 +- .../queries/findMatchById.server.ts | 76 +- .../queries/findResultsByMatchId.server.ts | 28 +- ...dTieBreakerMapPoolByTournamentId.server.ts | 6 +- .../insertTournamentMatchGameResult.server.ts | 4 +- ...namentMatchGameResultParticipant.server.ts | 6 +- .../playersThatPlayedByTeamId.server.ts | 6 +- .../updateMatchGameResultPoints.server.ts | 14 +- .../routes/to.$id.brackets.subscribe.tsx | 44 +- .../routes/to.$id.brackets.tsx | 1360 +- .../routes/to.$id.matches.$mid.subscribe.tsx | 40 +- .../routes/to.$id.matches.$mid.tsx | 1502 +- .../tournament-bracket-schemas.server.ts | 216 +- .../tournament-bracket-utils.test.ts | 86 +- .../tournament-bracket-utils.ts | 362 +- .../tournament-bracket/tournament-bracket.css | 702 +- .../queries/deleteSub.server.ts | 4 +- .../queries/findSubsByTournamentId.server.ts | 50 +- .../queries/upsertSub.server.ts | 2 +- .../routes/to.$id.subs.new.tsx | 550 +- .../tournament-subs/routes/to.$id.subs.tsx | 353 +- .../tournament-subs-constants.ts | 4 +- .../tournament-subs-schemas.server.ts | 72 +- .../tournament-subs/tournament-subs.css | 120 +- .../tournament/TournamentRepository.server.ts | 1126 +- .../TournamentTeamRepository.server.ts | 318 +- .../actions/to.$id.register.server.ts | 394 +- .../tournament/components/TeamWithRoster.tsx | 264 +- .../components/TournamentStream.tsx | 110 +- app/features/tournament/core/sets.server.ts | 320 +- .../tournament/core/streams.server.ts | 61 +- .../loaders/to.$id.register.server.ts | 36 +- .../queries/changeTeamOwner.server.ts | 34 +- .../tournament/queries/checkIn.server.ts | 2 +- .../tournament/queries/checkInMany.server.ts | 26 +- .../tournament/queries/checkOut.server.ts | 2 +- .../createSwissBracketInTransaction.server.ts | 100 +- .../tournament/queries/deleteTeam.server.ts | 4 +- .../queries/deleteTeamMember.server.ts | 10 +- .../queries/findByIdentifier.server.ts | 52 +- .../findMapPoolsByTournamentId.server.ts | 20 +- .../queries/findOwnTournamentTeam.server.ts | 22 +- .../findRoundsByTournamentId.server.ts | 14 +- .../queries/findTeamByInviteCode.server.ts | 8 +- .../tournament/queries/giveTrust.server.ts | 10 +- .../queries/hasTournamentFinalized.server.ts | 2 +- .../queries/hasTournamentStarted.server.ts | 2 +- .../queries/joinLeaveTeam.server.ts | 90 +- .../queries/setHistoryByTeamId.server.ts | 60 +- .../queries/updateRoundMaps.server.ts | 14 +- .../queries/updateTeamSeeds.server.ts | 18 +- .../queries/upsertCounterpickMaps.server.ts | 32 +- .../tournament/routes/to.$id.admin.tsx | 1706 +- .../tournament/routes/to.$id.index.tsx | 12 +- .../tournament/routes/to.$id.join.tsx | 400 +- .../tournament/routes/to.$id.register.tsx | 2044 +- .../tournament/routes/to.$id.seeds.tsx | 564 +- .../tournament/routes/to.$id.streams.tsx | 56 +- .../tournament/routes/to.$id.teams.$tid.tsx | 454 +- .../tournament/routes/to.$id.teams.tsx | 36 +- app/features/tournament/routes/to.$id.tsx | 383 +- .../tournament/tournament-constants.ts | 34 +- .../tournament/tournament-schemas.server.ts | 264 +- .../tournament/tournament-utils.server.ts | 18 +- app/features/tournament/tournament-utils.ts | 554 +- app/features/tournament/tournament.css | 626 +- .../user-page/UserRepository.server.ts | 950 +- .../u.$identifier.builds.new.server.ts | 284 +- .../actions/u.$identifier.builds.server.ts | 46 +- .../user-page/components/UserResultsTable.tsx | 262 +- .../u.$identifier.builds.new.server.ts | 60 +- .../loaders/u.$identifier.builds.server.ts | 54 +- .../user-page/routes/u.$identifier.art.tsx | 388 +- .../routes/u.$identifier.builds.new.tsx | 506 +- .../user-page/routes/u.$identifier.builds.tsx | 266 +- .../user-page/routes/u.$identifier.edit.tsx | 1136 +- .../user-page/routes/u.$identifier.index.tsx | 502 +- .../u.$identifier.results.highlights.tsx | 106 +- .../routes/u.$identifier.results.tsx | 98 +- .../routes/u.$identifier.seasons.tsx | 1382 +- .../user-page/routes/u.$identifier.tsx | 336 +- .../user-page/routes/u.$identifier.vods.tsx | 70 +- .../user-page/user-page-schemas.server.ts | 12 +- app/features/user-search/routes/u.tsx | 172 +- app/features/vods/components/VodListing.tsx | 94 +- app/features/vods/components/VodPov.tsx | 20 +- app/features/vods/queries/createVod.server.ts | 98 +- .../vods/queries/findVodById.server.ts | 44 +- app/features/vods/queries/findVods.server.ts | 82 +- app/features/vods/routes/vods.$id.tsx | 346 +- app/features/vods/routes/vods.new.tsx | 998 +- app/features/vods/routes/vods.tsx | 314 +- app/features/vods/vods-constants.ts | 18 +- app/features/vods/vods-schemas.ts | 94 +- app/features/vods/vods-types.ts | 36 +- app/features/vods/vods-utils.ts | 46 +- app/features/vods/vods.css | 98 +- app/hooks/swr.ts | 100 +- app/hooks/useAutoRefresh.ts | 34 +- app/hooks/useAutoRerender.ts | 20 +- app/hooks/useBaseUrl.ts | 4 +- app/hooks/useEventListener.ts | 98 +- app/hooks/useIsMounted.ts | 10 +- app/hooks/useIsomorphicLayoutEffect.ts | 2 +- app/hooks/useOnClickOutside.ts | 32 +- app/hooks/usePagination.ts | 98 +- app/hooks/usePlannerBg.ts | 54 +- app/hooks/useRootLoaderData.ts | 8 +- app/hooks/useSearchParamState.ts | 94 +- app/hooks/useSetTitle.ts | 6 +- app/hooks/useTimeoutState.ts | 44 +- app/hooks/useVisibilityChange.ts | 22 +- app/hooks/useWindowSize.ts | 37 +- app/modules/brackets-manager/base/getter.ts | 1398 +- app/modules/brackets-manager/base/updater.ts | 444 +- app/modules/brackets-manager/create.ts | 1783 +- app/modules/brackets-manager/delete.ts | 88 +- app/modules/brackets-manager/find.ts | 254 +- app/modules/brackets-manager/get.ts | 439 +- app/modules/brackets-manager/helpers.ts | 1447 +- app/modules/brackets-manager/index.ts | 50 +- app/modules/brackets-manager/manager.ts | 196 +- app/modules/brackets-manager/ordering.ts | 242 +- app/modules/brackets-manager/reset.ts | 84 +- .../brackets-manager/test/custom.test.ts | 108 +- .../brackets-manager/test/delete.test.ts | 130 +- .../test/double-elimination.test.ts | 628 +- .../brackets-manager/test/find.test.ts | 328 +- .../brackets-manager/test/general.test.ts | 728 +- app/modules/brackets-manager/test/get.test.ts | 126 +- .../brackets-manager/test/helpers.test.ts | 482 +- .../brackets-manager/test/round-robin.test.ts | 386 +- .../test/single-elimination.test.ts | 506 +- .../brackets-manager/test/update.test.ts | 434 +- app/modules/brackets-manager/types.ts | 218 +- app/modules/brackets-manager/update.ts | 188 +- app/modules/brackets-memory-db/index.ts | 460 +- app/modules/brackets-model/input.ts | 162 +- app/modules/brackets-model/other.ts | 56 +- app/modules/brackets-model/storage.ts | 82 +- app/modules/brackets-model/unions.ts | 36 +- app/modules/i18n/config.ts | 254 +- app/modules/i18n/i18next.server.ts | 32 +- app/modules/i18n/loader.server.ts | 34 +- app/modules/i18n/loader.ts | 46 +- app/modules/in-game-lists/abilities.ts | 148 +- app/modules/in-game-lists/gear-ids.ts | 134 +- app/modules/in-game-lists/index.ts | 100 +- app/modules/in-game-lists/modes.ts | 10 +- app/modules/in-game-lists/stage-ids.ts | 96 +- app/modules/in-game-lists/types.ts | 48 +- app/modules/in-game-lists/utils.ts | 2 +- app/modules/in-game-lists/weapon-alt-names.ts | 90 +- app/modules/in-game-lists/weapon-ids.ts | 350 +- app/modules/patreon/constants.ts | 2 +- app/modules/patreon/schema.ts | 126 +- app/modules/patreon/updater.ts | 239 +- .../constants.ts | 168 +- .../generation.test.ts | 1370 +- .../tournament-map-list-generator/index.ts | 8 +- .../starter-map.ts | 106 +- .../tournament-map-list.ts | 801 +- .../tournament-map-list-generator/types.ts | 44 +- .../tournament-map-list-generator/utils.ts | 99 +- app/modules/twitch/schemas.ts | 46 +- app/modules/twitch/streams.ts | 167 +- app/modules/twitch/token.ts | 51 +- app/modules/twitch/utils.ts | 22 +- app/permissions.ts | 360 +- app/root.tsx | 802 +- app/styles/badges.css | 138 +- app/styles/builds.css | 50 +- app/styles/calendar-event.css | 70 +- app/styles/calendar-new.css | 40 +- app/styles/calendar.css | 180 +- app/styles/common.css | 1854 +- app/styles/faq.css | 16 +- app/styles/flags.css | 502 +- app/styles/front.css | 206 +- app/styles/layout.css | 459 +- app/styles/maps.css | 140 +- app/styles/plus-history.css | 78 +- app/styles/plus.css | 136 +- app/styles/reset.css | 18 +- app/styles/u-edit.css | 26 +- app/styles/u.css | 508 +- app/styles/utils.css | 230 +- app/styles/vars.css | 284 +- app/utils/Test.ts | 166 +- app/utils/arrays.ts | 86 +- app/utils/cache.server.ts | 24 +- app/utils/dates.ts | 122 +- app/utils/fetch.ts | 22 +- app/utils/flip.ts | 16 +- app/utils/i18n.server.ts | 10 +- app/utils/i18next.ts | 4 +- app/utils/invariant.ts | 10 +- app/utils/json.ts | 10 +- app/utils/kysely.server.ts | 18 +- app/utils/logger.ts | 7 +- app/utils/newrelic.server.ts | 34 +- app/utils/number.ts | 32 +- app/utils/numbers.test.ts | 28 +- app/utils/playwright.ts | 102 +- app/utils/remix.ts | 377 +- app/utils/sql.ts | 30 +- app/utils/string.test.ts | 4 +- app/utils/strings.ts | 70 +- app/utils/types.ts | 22 +- app/utils/unslugify.server.ts | 16 +- app/utils/urls.ts | 414 +- app/utils/users.test.ts | 58 +- app/utils/users.ts | 74 +- app/utils/zod.ts | 224 +- biome.json | 53 + e2e/analyzer.spec.ts | 118 +- e2e/builds.spec.ts | 226 +- e2e/lfg.spec.ts | 28 +- e2e/object-damage-calculator.spec.ts | 102 +- e2e/shared.ts | 18 +- e2e/team.spec.ts | 192 +- e2e/top-search.spec.ts | 42 +- e2e/tournament-bracket.spec.ts | 1724 +- e2e/tournament-staff.spec.ts | 202 +- e2e/tournament.spec.ts | 310 +- e2e/user-page.spec.ts | 164 +- e2e/vods.spec.ts | 228 +- locales/da/analyzer.json | 304 +- locales/da/art.json | 54 +- locales/da/badges.json | 16 +- locales/da/builds.json | 68 +- locales/da/calendar.json | 122 +- locales/da/common.json | 338 +- locales/da/contributions.json | 18 +- locales/da/faq.json | 28 +- locales/da/game-misc.json | 118 +- locales/da/gear.json | 1650 +- locales/da/q.json | 24 +- locales/da/team.json | 74 +- locales/da/tournament.json | 62 +- locales/da/user.json | 70 +- locales/da/vods.json | 42 +- locales/da/weapons.json | 352 +- locales/de/analyzer.json | 228 +- locales/de/badges.json | 14 +- locales/de/builds.json | 22 +- locales/de/calendar.json | 108 +- locales/de/common.json | 300 +- locales/de/contributions.json | 16 +- locales/de/faq.json | 18 +- locales/de/game-misc.json | 120 +- locales/de/gear.json | 1650 +- locales/de/team.json | 72 +- locales/de/tournament.json | 38 +- locales/de/user.json | 56 +- locales/de/vods.json | 42 +- locales/de/weapons.json | 352 +- locales/en/analyzer.json | 320 +- locales/en/art.json | 54 +- locales/en/badges.json | 20 +- locales/en/builds.json | 72 +- locales/en/calendar.json | 122 +- locales/en/common.json | 410 +- locales/en/contributions.json | 20 +- locales/en/faq.json | 36 +- locales/en/game-misc.json | 120 +- locales/en/gear.json | 1650 +- locales/en/lfg.json | 56 +- locales/en/q.json | 336 +- locales/en/team.json | 74 +- locales/en/tournament.json | 258 +- locales/en/user.json | 120 +- locales/en/vods.json | 48 +- locales/en/weapons.json | 352 +- locales/es-ES/analyzer.json | 306 +- locales/es-ES/art.json | 54 +- locales/es-ES/badges.json | 20 +- locales/es-ES/builds.json | 68 +- locales/es-ES/calendar.json | 124 +- locales/es-ES/common.json | 392 +- locales/es-ES/contributions.json | 20 +- locales/es-ES/faq.json | 36 +- locales/es-ES/game-misc.json | 100 +- locales/es-ES/gear.json | 1650 +- locales/es-ES/q.json | 326 +- locales/es-ES/team.json | 74 +- locales/es-ES/tournament.json | 254 +- locales/es-ES/user.json | 106 +- locales/es-ES/vods.json | 48 +- locales/es-ES/weapons.json | 352 +- locales/es-US/analyzer.json | 306 +- locales/es-US/art.json | 54 +- locales/es-US/badges.json | 20 +- locales/es-US/builds.json | 68 +- locales/es-US/calendar.json | 124 +- locales/es-US/common.json | 392 +- locales/es-US/contributions.json | 20 +- locales/es-US/faq.json | 36 +- locales/es-US/game-misc.json | 120 +- locales/es-US/gear.json | 1650 +- locales/es-US/q.json | 326 +- locales/es-US/team.json | 74 +- locales/es-US/tournament.json | 254 +- locales/es-US/user.json | 106 +- locales/es-US/vods.json | 48 +- locales/es-US/weapons.json | 352 +- locales/fr-CA/analyzer.json | 298 +- locales/fr-CA/art.json | 52 +- locales/fr-CA/badges.json | 16 +- locales/fr-CA/builds.json | 52 +- locales/fr-CA/calendar.json | 122 +- locales/fr-CA/common.json | 340 +- locales/fr-CA/contributions.json | 18 +- locales/fr-CA/faq.json | 28 +- locales/fr-CA/game-misc.json | 120 +- locales/fr-CA/gear.json | 1650 +- locales/fr-CA/q.json | 18 +- locales/fr-CA/team.json | 74 +- locales/fr-CA/tournament.json | 222 +- locales/fr-CA/user.json | 70 +- locales/fr-CA/vods.json | 42 +- locales/fr-CA/weapons.json | 352 +- locales/fr-EU/analyzer.json | 298 +- locales/fr-EU/art.json | 52 +- locales/fr-EU/badges.json | 16 +- locales/fr-EU/builds.json | 52 +- locales/fr-EU/calendar.json | 122 +- locales/fr-EU/common.json | 340 +- locales/fr-EU/contributions.json | 18 +- locales/fr-EU/faq.json | 28 +- locales/fr-EU/game-misc.json | 120 +- locales/fr-EU/gear.json | 1650 +- locales/fr-EU/q.json | 18 +- locales/fr-EU/team.json | 74 +- locales/fr-EU/tournament.json | 222 +- locales/fr-EU/user.json | 70 +- locales/fr-EU/vods.json | 42 +- locales/fr-EU/weapons.json | 352 +- locales/he/analyzer.json | 302 +- locales/he/art.json | 54 +- locales/he/badges.json | 16 +- locales/he/builds.json | 52 +- locales/he/calendar.json | 122 +- locales/he/common.json | 344 +- locales/he/contributions.json | 18 +- locales/he/faq.json | 32 +- locales/he/game-misc.json | 118 +- locales/he/gear.json | 1650 +- locales/he/q.json | 24 +- locales/he/team.json | 74 +- locales/he/tournament.json | 224 +- locales/he/user.json | 70 +- locales/he/vods.json | 42 +- locales/he/weapons.json | 352 +- locales/it/analyzer.json | 216 +- locales/it/badges.json | 14 +- locales/it/builds.json | 22 +- locales/it/calendar.json | 96 +- locales/it/common.json | 188 +- locales/it/contributions.json | 12 +- locales/it/faq.json | 18 +- locales/it/game-misc.json | 120 +- locales/it/gear.json | 1650 +- locales/it/tournament.json | 38 +- locales/it/user.json | 50 +- locales/it/weapons.json | 352 +- locales/ja/analyzer.json | 320 +- locales/ja/art.json | 54 +- locales/ja/badges.json | 20 +- locales/ja/builds.json | 72 +- locales/ja/calendar.json | 122 +- locales/ja/common.json | 404 +- locales/ja/contributions.json | 20 +- locales/ja/faq.json | 36 +- locales/ja/game-misc.json | 120 +- locales/ja/gear.json | 1650 +- locales/ja/lfg.json | 56 +- locales/ja/q.json | 336 +- locales/ja/team.json | 74 +- locales/ja/tournament.json | 256 +- locales/ja/user.json | 116 +- locales/ja/vods.json | 48 +- locales/ja/weapons.json | 352 +- locales/ko/analyzer.json | 296 +- locales/ko/badges.json | 16 +- locales/ko/builds.json | 52 +- locales/ko/calendar.json | 88 +- locales/ko/common.json | 282 +- locales/ko/contributions.json | 18 +- locales/ko/faq.json | 6 +- locales/ko/game-misc.json | 120 +- locales/ko/gear.json | 1650 +- locales/ko/user.json | 14 +- locales/ko/weapons.json | 352 +- locales/nl/analyzer.json | 178 +- locales/nl/badges.json | 14 +- locales/nl/builds.json | 22 +- locales/nl/calendar.json | 96 +- locales/nl/common.json | 108 +- locales/nl/contributions.json | 10 +- locales/nl/faq.json | 6 +- locales/nl/game-misc.json | 120 +- locales/nl/gear.json | 1650 +- locales/nl/user.json | 40 +- locales/nl/weapons.json | 352 +- locales/pl/analyzer.json | 228 +- locales/pl/badges.json | 14 +- locales/pl/builds.json | 22 +- locales/pl/calendar.json | 96 +- locales/pl/common.json | 288 +- locales/pl/contributions.json | 16 +- locales/pl/faq.json | 18 +- locales/pl/game-misc.json | 118 +- locales/pl/gear.json | 1650 +- locales/pl/team.json | 70 +- locales/pl/tournament.json | 38 +- locales/pl/user.json | 56 +- locales/pl/weapons.json | 352 +- locales/pt-BR/analyzer.json | 302 +- locales/pt-BR/art.json | 54 +- locales/pt-BR/badges.json | 16 +- locales/pt-BR/builds.json | 52 +- locales/pt-BR/calendar.json | 122 +- locales/pt-BR/common.json | 346 +- locales/pt-BR/contributions.json | 20 +- locales/pt-BR/faq.json | 36 +- locales/pt-BR/game-misc.json | 118 +- locales/pt-BR/gear.json | 1650 +- locales/pt-BR/lfg.json | 56 +- locales/pt-BR/q.json | 24 +- locales/pt-BR/team.json | 74 +- locales/pt-BR/tournament.json | 224 +- locales/pt-BR/user.json | 70 +- locales/pt-BR/vods.json | 42 +- locales/pt-BR/weapons.json | 352 +- locales/ru/analyzer.json | 228 +- locales/ru/art.json | 54 +- locales/ru/badges.json | 16 +- locales/ru/builds.json | 50 +- locales/ru/calendar.json | 122 +- locales/ru/common.json | 372 +- locales/ru/contributions.json | 16 +- locales/ru/faq.json | 18 +- locales/ru/game-misc.json | 120 +- locales/ru/gear.json | 1650 +- locales/ru/q.json | 310 +- locales/ru/team.json | 74 +- locales/ru/tournament.json | 38 +- locales/ru/user.json | 106 +- locales/ru/vods.json | 42 +- locales/ru/weapons.json | 352 +- locales/zh/analyzer.json | 302 +- locales/zh/art.json | 54 +- locales/zh/badges.json | 16 +- locales/zh/builds.json | 52 +- locales/zh/calendar.json | 122 +- locales/zh/common.json | 344 +- locales/zh/contributions.json | 20 +- locales/zh/faq.json | 34 +- locales/zh/game-misc.json | 120 +- locales/zh/gear.json | 1650 +- locales/zh/q.json | 24 +- locales/zh/team.json | 74 +- locales/zh/tournament.json | 224 +- locales/zh/user.json | 70 +- locales/zh/vods.json | 42 +- locales/zh/weapons.json | 352 +- migrations/000-initial.js | 60 +- migrations/001-badges.js | 326 +- migrations/002-fix-plus-tier.js | 16 +- migrations/003-badges-hue.js | 6 +- migrations/004-itz-badges.js | 40 +- migrations/005-patrons.js | 50 +- migrations/006-calendar.js | 48 +- migrations/007-builds.js | 38 +- migrations/008-user-new-fields.js | 24 +- migrations/009-maps.js | 42 +- migrations/010-fix-maps.js | 60 +- migrations/011-user-result-highlights.js | 20 +- migrations/012-to-tools.js | 94 +- migrations/013-weapon-pool.js | 14 +- migrations/014-full-tournament.js | 100 +- migrations/015-teams.js | 38 +- migrations/016-adjust-submitted-img.js | 18 +- migrations/017-persisted-plus-tier.js | 20 +- migrations/018-add-user-team-css.js | 4 +- migrations/019-add-patron-till.js | 2 +- migrations/020-vods.js | 54 +- migrations/021-abilities.js | 6 +- migrations/022-xrank.js | 24 +- migrations/023-add-user-fav-badge.js | 6 +- migrations/024-adjust-tournament.js | 18 +- migrations/025-private-builds.js | 6 +- migrations/026-full-tournament-v2.js | 214 +- migrations/027-tournament-subs.js | 6 +- migrations/028-user-changes.js | 14 +- migrations/029-end-tournament.js | 88 +- migrations/030-art.js | 42 +- migrations/031-art-tags.js | 16 +- migrations/032-q.js | 176 +- migrations/033-banned.js | 6 +- migrations/034-vc.js | 8 +- migrations/035-log-in-link.js | 6 +- migrations/036-add-chat-code.js | 8 +- migrations/037-add-memento.js | 6 +- migrations/038-group-member-note.js | 6 +- migrations/039-tournament-match-chat-code.js | 10 +- migrations/040-indexes.js | 22 +- migrations/041-plus-sixty.js | 14 +- migrations/042-plus-skipped.js | 6 +- migrations/043-q-new-map-pick.js | 26 +- migrations/044-no-screen.js | 6 +- migrations/045-tournament-staff.js | 12 +- migrations/046-tournament-progression.js | 26 +- migrations/047-bracket-stuff.js | 22 +- migrations/048-season-3-sq.js | 20 +- migrations/049-tournament-round-maps.js | 2 +- migrations/050-tournament-no-screen.js | 6 +- migrations/051-pick-ban.js | 30 +- migrations/052-indexes.js | 12 +- migrations/053-banned-reason.js | 6 +- migrations/054-swiss.js | 16 +- migrations/055-lfg.js | 22 +- migrations/056-skill-index.js | 10 +- migrations/057-calendar-event-avatar.js | 16 +- migrations/058-active-roster.js | 10 +- migrations/059-user-name.js | 18 +- migrations/060-tournament-team-member-ign.js | 10 +- migrations/061-fc-index.js | 10 +- migrations/062-tournament-team-avatar.js | 10 +- migrations/063-user-battlefy.js | 6 +- package-lock.json | 57974 ++++--- package.json | 231 +- playwright.config.ts | 174 +- public/app.webmanifest | 98 +- public/sw.js | 4 +- scripts/add-badge-winners.ts | 26 +- scripts/add-badge.ts | 14 +- scripts/check-translation-jsons.ts | 560 +- scripts/create-analyzer-json.ts | 1387 +- scripts/create-gear-json.ts | 195 +- scripts/create-misc-json.ts | 205 +- scripts/create-object-dmg-json.ts | 129 +- scripts/delete-badge.ts | 20 +- scripts/delete-db-files.mjs | 24 +- scripts/delete-skill.ts | 14 +- scripts/delete-tournament-team-maps.ts | 10 +- scripts/delete-user.ts | 10 +- scripts/disband-team.ts | 18 +- scripts/fc-lookup.ts | 46 +- scripts/generate-weapon-csv.js | 55 - scripts/hex-to-filter.ts | 603 +- scripts/map-popularity-q.ts | 184 +- scripts/map-popularity.ts | 168 +- scripts/nuke-reported-weapons.ts | 92 +- scripts/placements/index.ts | 204 +- scripts/placements/schemas.ts | 136 +- scripts/recent-fcs.ts | 86 +- scripts/refresh-prod-db.ts | 49 +- scripts/remove-vodder.ts | 20 +- scripts/rename-badge.ts | 14 +- scripts/reopen-tournament.ts | 14 +- scripts/replace-img-names.ts | 86 +- scripts/replace-weapon-names.ts | 94 +- scripts/season-initial-powers.ts | 108 +- scripts/skip-plus.ts | 16 +- scripts/swap-users.ts | 29 +- scripts/sync-xp-badges.ts | 4 +- scripts/transfer-weapon-pools.ts | 70 +- scripts/unlink-player.ts | 14 +- scripts/unskip-plus.ts | 14 +- scripts/update-fc.ts | 26 +- scripts/utils.ts | 59 +- tsconfig.json | 59 +- types/react-i18next.d.ts | 72 +- types/react.d.ts | 7 +- vite.config.ts | 616 +- 1070 files changed, 219485 insertions(+), 224310 deletions(-) delete mode 100644 .eslintrc.cjs delete mode 100644 .prettierignore delete mode 100644 .stylelintrc.json create mode 100644 biome.json delete mode 100644 scripts/generate-weapon-csv.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 8f55a4f01..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = { - root: true, - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - }, - plugins: ["@typescript-eslint"], - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - ], - rules: { - "@typescript-eslint/no-unused-vars": [ - "warn", - { argsIgnorePattern: "^_", ignoreRestSiblings: true }, - ], - "no-constant-condition": ["error", { checkLoops: false }], - "no-console": ["warn", { allow: ["warn", "error"] }], - "no-warning-comments": ["warn", { terms: ["xxx"] }], - "no-var": 0, - "no-useless-escape": 0, - "@typescript-eslint/no-unsafe-return": 0, - "@typescript-eslint/no-unsafe-member-access": 0, - "@typescript-eslint/no-unsafe-assignment": 0, - "@typescript-eslint/no-unsafe-call": 0, - "@typescript-eslint/no-unsafe-argument": 0, - "@typescript-eslint/no-non-null-assertion": 0, - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/unbound-method": 0, - "@typescript-eslint/consistent-type-imports": "warn", - "react/prop-types": 0, - "react/display-name": 0, - }, - settings: { - react: { - version: "detect", - }, - }, -}; diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 20f614c2d..c3c26c005 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,12 +27,8 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts - - name: Prettier - run: npm run prettier:check - - name: Lint TS - run: npm run lint:ts - - name: Stylelint - run: npm run lint:css + - name: Formatter/Linter + run: npm run biome:check - name: Typecheck run: npm run typecheck - name: Check translations jsons diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index c795b054e..000000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -build \ No newline at end of file diff --git a/.stylelintrc.json b/.stylelintrc.json deleted file mode 100644 index bcffe32a8..000000000 --- a/.stylelintrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": ["stylelint-config-standard"], - "rules": { - "selector-pseudo-class-no-unknown": [ - true, - { - "ignorePseudoClasses": ["global"] - } - ], - "selector-class-pattern": ["[a-z-_]+"], - "custom-property-pattern": "(?<=_?)", - "media-feature-range-notation": "prefix", - "property-no-vendor-prefix": [ - true, - { - "ignoreProperties": ["backdrop-filter"] - } - ] - } -} diff --git a/README.md b/README.md index 929b3f7f8..4f7760d74 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ Note: This is only useful if you have access to a production running on Render.c ### Fix errors from the CI Pipeline -If you change any files and the CI pipeline errors out on certain formatting/linting steps (e.g. the `Prettier` or `Stylelint` step), run this command in the repo's root directory: +If you change any files and the CI pipeline errors out on certain formatting/linting steps (Biome) run this command in the repo's root directory: ```sh npm run cf diff --git a/app/components/AbilitiesSelector.tsx b/app/components/AbilitiesSelector.tsx index cd8c85188..340adc644 100644 --- a/app/components/AbilitiesSelector.tsx +++ b/app/components/AbilitiesSelector.tsx @@ -1,209 +1,209 @@ import clsx from "clsx"; import * as React from "react"; -import invariant from "~/utils/invariant"; import { abilities } from "~/modules/in-game-lists"; import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists/types"; +import invariant from "~/utils/invariant"; import { abilityImageUrl } from "~/utils/urls"; import { Ability } from "./Ability"; import { Image } from "./Image"; interface AbilitiesSelectorProps { - selectedAbilities: BuildAbilitiesTupleWithUnknown; - onChange: (newAbilities: BuildAbilitiesTupleWithUnknown) => void; + selectedAbilities: BuildAbilitiesTupleWithUnknown; + onChange: (newAbilities: BuildAbilitiesTupleWithUnknown) => void; } export function AbilitiesSelector({ - selectedAbilities, - onChange, + selectedAbilities, + onChange, }: AbilitiesSelectorProps) { - const [, startTransition] = React.useTransition(); + const [, startTransition] = React.useTransition(); - const onSlotClick = ({ - rowI, - abilityI, - }: { - rowI: number; - abilityI: number; - }) => { - const abilitiesClone = JSON.parse( - JSON.stringify(selectedAbilities), - ) as BuildAbilitiesTupleWithUnknown; + const onSlotClick = ({ + rowI, + abilityI, + }: { + rowI: number; + abilityI: number; + }) => { + const abilitiesClone = JSON.parse( + JSON.stringify(selectedAbilities), + ) as BuildAbilitiesTupleWithUnknown; - const row = abilitiesClone[rowI]; - invariant(row); - invariant(row.length === 4); + const row = abilitiesClone[rowI]; + invariant(row); + invariant(row.length === 4); - // no need to trigger a rerender - if (row[abilityI] === "UNKNOWN") return; + // no need to trigger a rerender + if (row[abilityI] === "UNKNOWN") return; - row[abilityI] = "UNKNOWN"; + row[abilityI] = "UNKNOWN"; - onChange(abilitiesClone); - }; - const onButtonClick = (ability: (typeof abilities)[number]) => { - startTransition(() => { - onChange(addAbility({ oldAbilities: selectedAbilities, ability })); - }); - }; + onChange(abilitiesClone); + }; + const onButtonClick = (ability: (typeof abilities)[number]) => { + startTransition(() => { + onChange(addAbility({ oldAbilities: selectedAbilities, ability })); + }); + }; - const [draggingAbility, setDraggingAbility] = React.useState< - (typeof abilities)[number] | undefined - >(); + const [draggingAbility, setDraggingAbility] = React.useState< + (typeof abilities)[number] | undefined + >(); - const onDragStart = - (ability: (typeof abilities)[number]) => (event: React.DragEvent) => { - setDraggingAbility(ability); - event.dataTransfer.setData("text/plain", JSON.stringify(ability)); - }; + const onDragStart = + (ability: (typeof abilities)[number]) => (event: React.DragEvent) => { + setDraggingAbility(ability); + event.dataTransfer.setData("text/plain", JSON.stringify(ability)); + }; - const onDragEnd = () => { - setDraggingAbility(undefined); - }; + const onDragEnd = () => { + setDraggingAbility(undefined); + }; - const onDrop = - (atRowIndex: number, atAbilityIndex: number) => - (event: React.DragEvent) => { - event.preventDefault(); - const ability = JSON.parse( - event.dataTransfer.getData("text/plain"), - ) as (typeof abilities)[number]; + const onDrop = + (atRowIndex: number, atAbilityIndex: number) => + (event: React.DragEvent) => { + event.preventDefault(); + const ability = JSON.parse( + event.dataTransfer.getData("text/plain"), + ) as (typeof abilities)[number]; - onChange( - addAbility({ - oldAbilities: selectedAbilities, - ability, - atRowIndex, - atAbilityIndex, - }), - ); - }; + onChange( + addAbility({ + oldAbilities: selectedAbilities, + ability, + atRowIndex, + atAbilityIndex, + }), + ); + }; - return ( -
-
- {selectedAbilities.map((row, rowI) => - row.map((ability, abilityI) => ( - onSlotClick({ rowI, abilityI })} - dragStarted={!!draggingAbility} - dropAllowed={canPlaceAbilityAtSlot( - rowI, - abilityI, - draggingAbility, - )} - onDrop={onDrop(rowI, abilityI)} - /> - )), - )} -
-
- {abilities.map((ability) => ( - - ))} -
-
- ); + return ( +
+
+ {selectedAbilities.map((row, rowI) => + row.map((ability, abilityI) => ( + onSlotClick({ rowI, abilityI })} + dragStarted={!!draggingAbility} + dropAllowed={canPlaceAbilityAtSlot( + rowI, + abilityI, + draggingAbility, + )} + onDrop={onDrop(rowI, abilityI)} + /> + )), + )} +
+
+ {abilities.map((ability) => ( + + ))} +
+
+ ); } const canPlaceAbilityAtSlot = ( - rowIndex: number, - abilityIndex: number, - ability?: (typeof abilities)[number], + rowIndex: number, + abilityIndex: number, + ability?: (typeof abilities)[number], ) => { - if (!ability) { - return false; - } + if (!ability) { + return false; + } - const legalGearTypeForMain = - rowIndex === 0 - ? "HEAD_MAIN_ONLY" - : rowIndex === 1 - ? "CLOTHES_MAIN_ONLY" - : "SHOES_MAIN_ONLY"; + const legalGearTypeForMain = + rowIndex === 0 + ? "HEAD_MAIN_ONLY" + : rowIndex === 1 + ? "CLOTHES_MAIN_ONLY" + : "SHOES_MAIN_ONLY"; - const isMainSlot = abilityIndex === 0; + const isMainSlot = abilityIndex === 0; - if ( - !["STACKABLE", legalGearTypeForMain].includes(ability.type) && - isMainSlot - ) { - // Can't put this type of gear in main slot - return false; - } + if ( + !["STACKABLE", legalGearTypeForMain].includes(ability.type) && + isMainSlot + ) { + // Can't put this type of gear in main slot + return false; + } - if (!isMainSlot && ability.type !== "STACKABLE") { - // Can't put main slot only gear to sub slots - return false; - } - return true; + if (!isMainSlot && ability.type !== "STACKABLE") { + // Can't put main slot only gear to sub slots + return false; + } + return true; }; function addAbility({ - oldAbilities, - ability, - atRowIndex, - atAbilityIndex, + oldAbilities, + ability, + atRowIndex, + atAbilityIndex, }: { - oldAbilities: BuildAbilitiesTupleWithUnknown; - ability: (typeof abilities)[number]; - atRowIndex?: number; - atAbilityIndex?: number; + oldAbilities: BuildAbilitiesTupleWithUnknown; + ability: (typeof abilities)[number]; + atRowIndex?: number; + atAbilityIndex?: number; }): BuildAbilitiesTupleWithUnknown { - const abilitiesClone = JSON.parse( - JSON.stringify(oldAbilities), - ) as BuildAbilitiesTupleWithUnknown; + const abilitiesClone = JSON.parse( + JSON.stringify(oldAbilities), + ) as BuildAbilitiesTupleWithUnknown; - if (atRowIndex !== undefined && atAbilityIndex !== undefined) { - // Attempt to place the ability at a specific slot since we - // were given an atRowIndex and atAbilityIndex - if (canPlaceAbilityAtSlot(atRowIndex, atAbilityIndex, ability)) { - // Assign this ability to the slot - abilitiesClone[atRowIndex][atAbilityIndex] = ability.name; - } - } else { - // Loop through all slots and attempt to place this ability - // in the first empty one - for (const [rowIndex, row] of abilitiesClone.entries()) { - for (const [abilityIndex, oldAbility] of row.entries()) { - if (oldAbility !== "UNKNOWN") { - // Skip any filled slots in this loop until we arrive at an empty one. - continue; - } + if (atRowIndex !== undefined && atAbilityIndex !== undefined) { + // Attempt to place the ability at a specific slot since we + // were given an atRowIndex and atAbilityIndex + if (canPlaceAbilityAtSlot(atRowIndex, atAbilityIndex, ability)) { + // Assign this ability to the slot + abilitiesClone[atRowIndex][atAbilityIndex] = ability.name; + } + } else { + // Loop through all slots and attempt to place this ability + // in the first empty one + for (const [rowIndex, row] of abilitiesClone.entries()) { + for (const [abilityIndex, oldAbility] of row.entries()) { + if (oldAbility !== "UNKNOWN") { + // Skip any filled slots in this loop until we arrive at an empty one. + continue; + } - if (!canPlaceAbilityAtSlot(rowIndex, abilityIndex, ability)) { - // This ability isn't valid for this slot - continue; - } + if (!canPlaceAbilityAtSlot(rowIndex, abilityIndex, ability)) { + // This ability isn't valid for this slot + continue; + } - // Assign this ability to the slot - abilitiesClone[rowIndex][abilityIndex] = ability.name; + // Assign this ability to the slot + abilitiesClone[rowIndex][abilityIndex] = ability.name; - return abilitiesClone; - } - } - } + return abilitiesClone; + } + } + } - // no-op if no available slots - return abilitiesClone; + // no-op if no available slots + return abilitiesClone; } diff --git a/app/components/Ability.tsx b/app/components/Ability.tsx index c88951829..9889e9c6b 100644 --- a/app/components/Ability.tsx +++ b/app/components/Ability.tsx @@ -1,89 +1,89 @@ import clsx from "clsx"; import React from "react"; +import { useTranslation } from "react-i18next"; import type { AbilityWithUnknown } from "~/modules/in-game-lists/types"; import { abilityImageUrl } from "~/utils/urls"; import { Image } from "./Image"; -import { useTranslation } from "react-i18next"; const sizeMap = { - MAIN: 42, - SUB: 32, - SUBTINY: 26, - TINY: 22, + MAIN: 42, + SUB: 32, + SUBTINY: 26, + TINY: 22, } as const; export function Ability({ - ability, - size, - dragStarted = false, - dropAllowed = false, - onClick, - onDrop, - className, + ability, + size, + dragStarted = false, + dropAllowed = false, + onClick, + onDrop, + className, }: { - ability: AbilityWithUnknown; - size: keyof typeof sizeMap; - dragStarted?: boolean; - dropAllowed?: boolean; - onClick?: () => void; - onDrop?: (event: React.DragEvent) => void; - className?: string; + ability: AbilityWithUnknown; + size: keyof typeof sizeMap; + dragStarted?: boolean; + dropAllowed?: boolean; + onClick?: () => void; + onDrop?: (event: React.DragEvent) => void; + className?: string; }) { - const { t } = useTranslation(["game-misc", "builds"]); - const sizeNumber = sizeMap[size]; + const { t } = useTranslation(["game-misc", "builds"]); + const sizeNumber = sizeMap[size]; - const [isDragTarget, setIsDragTarget] = React.useState(false); + const [isDragTarget, setIsDragTarget] = React.useState(false); - const onDragOver = (event: React.DragEvent) => { - event.preventDefault(); - setIsDragTarget(true); - }; + const onDragOver = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragTarget(true); + }; - const onDragLeave = () => { - setIsDragTarget(false); - }; + const onDragLeave = () => { + setIsDragTarget(false); + }; - const readonly = typeof onClick === "undefined" || ability === "UNKNOWN"; // Force "UNKNOWN" ability icons to be readonly + const readonly = typeof onClick === "undefined" || ability === "UNKNOWN"; // Force "UNKNOWN" ability icons to be readonly - // Render an ability as a button only if it is meant to be draggable (i.e., not readonly) - const AbilityTag = readonly ? "div" : "button"; + // Render an ability as a button only if it is meant to be draggable (i.e., not readonly) + const AbilityTag = readonly ? "div" : "button"; - const altText = - ability !== "UNKNOWN" - ? t(`game-misc:ABILITY_${ability}`) - : t("builds:emptyAbilitySlot"); + const altText = + ability !== "UNKNOWN" + ? t(`game-misc:ABILITY_${ability}`) + : t("builds:emptyAbilitySlot"); - return ( - { - setIsDragTarget(false); - onDrop?.(event); - }} - type={readonly ? undefined : "button"} - > - {altText} - - ); + return ( + { + setIsDragTarget(false); + onDrop?.(event); + }} + type={readonly ? undefined : "button"} + > + {altText} + + ); } diff --git a/app/components/Alert.tsx b/app/components/Alert.tsx index 3a2d6bbeb..d5860711b 100644 --- a/app/components/Alert.tsx +++ b/app/components/Alert.tsx @@ -1,51 +1,51 @@ import clsx from "clsx"; import type * as React from "react"; -import { AlertIcon } from "./icons/Alert"; -import { ErrorIcon } from "./icons/Error"; -import { CheckmarkIcon } from "./icons/Checkmark"; import { assertUnreachable } from "~/utils/types"; +import { AlertIcon } from "./icons/Alert"; +import { CheckmarkIcon } from "./icons/Checkmark"; +import { ErrorIcon } from "./icons/Error"; export type AlertVariation = "INFO" | "WARNING" | "ERROR" | "SUCCESS"; export function Alert({ - children, - textClassName, - alertClassName, - variation = "INFO", - tiny = false, + children, + textClassName, + alertClassName, + variation = "INFO", + tiny = false, }: { - children: React.ReactNode; - textClassName?: string; - alertClassName?: string; - variation?: AlertVariation; - tiny?: boolean; + children: React.ReactNode; + textClassName?: string; + alertClassName?: string; + variation?: AlertVariation; + tiny?: boolean; }) { - return ( -
- {" "} -
{children}
-
- ); + return ( +
+ {" "} +
{children}
+
+ ); } function Icon({ variation }: { variation: AlertVariation }) { - switch (variation) { - case "INFO": - return ; - case "WARNING": - return ; - case "ERROR": - return ; - case "SUCCESS": - return ; - default: - assertUnreachable(variation); - } + switch (variation) { + case "INFO": + return ; + case "WARNING": + return ; + case "ERROR": + return ; + case "SUCCESS": + return ; + default: + assertUnreachable(variation); + } } diff --git a/app/components/Avatar.tsx b/app/components/Avatar.tsx index 2a4193bd1..044e74499 100644 --- a/app/components/Avatar.tsx +++ b/app/components/Avatar.tsx @@ -1,62 +1,62 @@ import clsx from "clsx"; -import type { User } from "~/db/types"; import * as React from "react"; +import type { User } from "~/db/types"; import { BLANK_IMAGE_URL } from "~/utils/urls"; const dimensions = { - xxxs: 16, - xxs: 24, - xs: 36, - sm: 44, - xsm: 62, - md: 81, - lg: 125, + xxxs: 16, + xxs: 24, + xs: 36, + sm: 44, + xsm: 62, + md: 81, + lg: 125, } as const; function _Avatar({ - user, - url, - size = "sm", - className, - alt = "", - ...rest + user, + url, + size = "sm", + className, + alt = "", + ...rest }: { - user?: Pick; - url?: string; - className?: string; - alt?: string; - size: keyof typeof dimensions; + user?: Pick; + url?: string; + className?: string; + alt?: string; + size: keyof typeof dimensions; } & React.ButtonHTMLAttributes) { - const [isErrored, setIsErrored] = React.useState(false); - // TODO: just show text... my profile? - // TODO: also show this if discordAvatar is stale and 404's + const [isErrored, setIsErrored] = React.useState(false); + // TODO: just show text... my profile? + // TODO: also show this if discordAvatar is stale and 404's - React.useEffect(() => { - setIsErrored(false); - }, [user?.discordAvatar]); + // biome-ignore lint/correctness/useExhaustiveDependencies: every avatar error state is unique and we want to avoid using key on every avatar + React.useEffect(() => { + setIsErrored(false); + }, [user?.discordAvatar]); - const src = - url ?? - (user?.discordAvatar && !isErrored - ? `https://cdn.discordapp.com/avatars/${user.discordId}/${ - user.discordAvatar - }.webp${size === "lg" ? "?size=240" : "?size=80"}` - : BLANK_IMAGE_URL); // avoid broken image placeholder + const src = + url ?? + (user?.discordAvatar && !isErrored + ? `https://cdn.discordapp.com/avatars/${user.discordId}/${ + user.discordAvatar + }.webp${size === "lg" ? "?size=240" : "?size=80"}` + : BLANK_IMAGE_URL); // avoid broken image placeholder - return ( - {alt} setIsErrored(true)} - {...rest} - /> - ); + return ( + // biome-ignore lint/a11y/useAltText: spread messes it up https://github.com/biomejs/biome/issues/3081 + {alt} setIsErrored(true)} + {...rest} + /> + ); } export const Avatar = React.memo(_Avatar); diff --git a/app/components/Badge.tsx b/app/components/Badge.tsx index cdd3819d9..810a0f4cb 100644 --- a/app/components/Badge.tsx +++ b/app/components/Badge.tsx @@ -2,40 +2,41 @@ import { badgeUrl } from "~/utils/urls"; import { Image } from "./Image"; export function Badge({ - badge, - onClick, - isAnimated, - size, + badge, + onClick, + isAnimated, + size, }: { - badge: { displayName: string; hue?: number | null; code: string }; - onClick?: () => void; - isAnimated: boolean; - size: number; + badge: { displayName: string; hue?: number | null; code: string }; + onClick?: () => void; + isAnimated: boolean; + size: number; }) { - const commonProps = { - title: badge.displayName, - onClick, - width: size, - height: size, - style: badge.hue ? { filter: `hue-rotate(${badge.hue}deg)` } : undefined, - }; + const commonProps = { + title: badge.displayName, + onClick, + width: size, + height: size, + style: badge.hue ? { filter: `hue-rotate(${badge.hue}deg)` } : undefined, + }; - if (isAnimated) { - return ( - {badge.displayName} - ); - } + if (isAnimated) { + return ( + // biome-ignore lint/a11y/useAltText: false positive..? + {badge.displayName} + ); + } - return ( - {badge.displayName} - ); + return ( + {badge.displayName} + ); } diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx index ace1cc552..47fc71730 100644 --- a/app/components/BuildCard.tsx +++ b/app/components/BuildCard.tsx @@ -2,296 +2,296 @@ import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import type { - Build, - BuildWeapon, - GearType, - UserWithPlusTier, + Build, + BuildWeapon, + GearType, + UserWithPlusTier, } from "~/db/types"; -import { useIsMounted } from "~/hooks/useIsMounted"; import { useUser } from "~/features/auth/core/user"; +import type { BuildWeaponWithTop500Info } from "~/features/builds"; +import { useIsMounted } from "~/hooks/useIsMounted"; import type { - Ability as AbilityType, - ModeShort, + Ability as AbilityType, + ModeShort, } from "~/modules/in-game-lists"; import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types"; +import { altWeaponIdToId } from "~/modules/in-game-lists/weapon-ids"; import { databaseTimestampToDate } from "~/utils/dates"; import { gearTypeToInitial } from "~/utils/strings"; import { - analyzerPage, - gearImageUrl, - mainWeaponImageUrl, - modeImageUrl, - mySlugify, - navIconUrl, - userBuildsPage, - weaponBuildPage, + analyzerPage, + gearImageUrl, + mainWeaponImageUrl, + modeImageUrl, + mySlugify, + navIconUrl, + userBuildsPage, + weaponBuildPage, } from "~/utils/urls"; import { Ability } from "./Ability"; import { Button, LinkButton } from "./Button"; import { FormWithConfirm } from "./FormWithConfirm"; -import { TrashIcon } from "./icons/Trash"; -import { EditIcon } from "./icons/Edit"; import { Image } from "./Image"; import { Popover } from "./Popover"; +import { EditIcon } from "./icons/Edit"; import { InfoIcon } from "./icons/Info"; import { LockIcon } from "./icons/Lock"; -import type { BuildWeaponWithTop500Info } from "~/features/builds"; -import { altWeaponIdToId } from "~/modules/in-game-lists/weapon-ids"; +import { TrashIcon } from "./icons/Trash"; interface BuildProps { - build: Pick< - Build, - | "id" - | "title" - | "description" - | "clothesGearSplId" - | "headGearSplId" - | "shoesGearSplId" - | "updatedAt" - | "private" - > & { - abilities: BuildAbilitiesTuple; - modes: ModeShort[] | null; - weapons: Array<{ - weaponSplId: BuildWeapon["weaponSplId"]; - minRank: number | null; - maxPower: number | null; - }>; - }; - owner?: Pick; - canEdit?: boolean; + build: Pick< + Build, + | "id" + | "title" + | "description" + | "clothesGearSplId" + | "headGearSplId" + | "shoesGearSplId" + | "updatedAt" + | "private" + > & { + abilities: BuildAbilitiesTuple; + modes: ModeShort[] | null; + weapons: Array<{ + weaponSplId: BuildWeapon["weaponSplId"]; + minRank: number | null; + maxPower: number | null; + }>; + }; + owner?: Pick; + canEdit?: boolean; } export function BuildCard({ build, owner, canEdit = false }: BuildProps) { - const user = useUser(); - const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]); - const { i18n } = useTranslation(); - const isMounted = useIsMounted(); + const user = useUser(); + const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]); + const { i18n } = useTranslation(); + const isMounted = useIsMounted(); - const { - id, - title, - description, - clothesGearSplId, - headGearSplId, - shoesGearSplId, - updatedAt, - abilities, - modes, - weapons, - } = build; + const { + id, + title, + description, + clothesGearSplId, + headGearSplId, + shoesGearSplId, + updatedAt, + abilities, + modes, + weapons, + } = build; - return ( -
-
-
- {modes && modes.length > 0 && ( -
- {modes.map((mode) => ( - {t(`game-misc:MODE_LONG_${mode}` - ))} -
- )} -

- {title} -

-
-
- {owner && ( - <> - - {owner.username} - -
- - )} - {owner?.plusTier ? ( - <> - +{owner.plusTier} -
- - ) : null} -
- {build.private ? ( -
- {" "} - {t("common:build.private")} -
- ) : null} - -
-
-
-
- {weapons.map((weapon) => ( - - ))} - {weapons.length === 1 && ( -
- {t(`weapons:MAIN_${weapons[0].weaponSplId}` as any)} -
- )} -
-
- - - -
-
- - {t("common:pages.analyzer")} - - {description ? ( - } - triggerClassName="minimal tiny build__small-text" - > - {description} - - ) : null} - {canEdit && ( - <> - - - - - - - - )} -
-
- ); + return ( +
+
+
+ {modes && modes.length > 0 && ( +
+ {modes.map((mode) => ( + {t(`game-misc:MODE_LONG_${mode}` + ))} +
+ )} +

+ {title} +

+
+
+ {owner && ( + <> + + {owner.username} + +
+ + )} + {owner?.plusTier ? ( + <> + +{owner.plusTier} +
+ + ) : null} +
+ {build.private ? ( +
+ {" "} + {t("common:build.private")} +
+ ) : null} + +
+
+
+
+ {weapons.map((weapon) => ( + + ))} + {weapons.length === 1 && ( +
+ {t(`weapons:MAIN_${weapons[0].weaponSplId}` as any)} +
+ )} +
+
+ + + +
+
+ + {t("common:pages.analyzer")} + + {description ? ( + } + triggerClassName="minimal tiny build__small-text" + > + {description} + + ) : null} + {canEdit && ( + <> + + + + + + + + )} +
+
+ ); } function RoundWeaponImage({ weapon }: { weapon: BuildWeaponWithTop500Info }) { - const { weaponSplId, maxPower, minRank } = weapon; - const normalizedWeaponSplId = altWeaponIdToId.get(weaponSplId) ?? weaponSplId; + const { weaponSplId, maxPower, minRank } = weapon; + const normalizedWeaponSplId = altWeaponIdToId.get(weaponSplId) ?? weaponSplId; - const { t } = useTranslation(["weapons"]); - const slug = mySlugify( - t(`weapons:MAIN_${normalizedWeaponSplId}`, { lng: "en" }), - ); + const { t } = useTranslation(["weapons"]); + const slug = mySlugify( + t(`weapons:MAIN_${normalizedWeaponSplId}`, { lng: "en" }), + ); - const isTop500 = typeof maxPower === "number" && typeof minRank === "number"; + const isTop500 = typeof maxPower === "number" && typeof minRank === "number"; - return ( -
- {isTop500 ? ( - - ) : null} - - {t(`weapons:MAIN_${weaponSplId}` - -
- ); + return ( +
+ {isTop500 ? ( + + ) : null} + + {t(`weapons:MAIN_${weaponSplId}` + +
+ ); } function AbilitiesRowWithGear({ - gearType, - abilities, - gearId, + gearType, + abilities, + gearId, }: { - gearType: GearType; - abilities: AbilityType[]; - gearId: number; + gearType: GearType; + abilities: AbilityType[]; + gearId: number; }) { - const { t } = useTranslation(["gear"]); - const translatedGearName = t( - `gear:${gearTypeToInitial(gearType)}_${gearId}` as any, - ); + const { t } = useTranslation(["gear"]); + const translatedGearName = t( + `gear:${gearTypeToInitial(gearType)}_${gearId}` as any, + ); - return ( - <> - {translatedGearName} - {abilities.map((ability, i) => ( - - ))} - - ); + return ( + <> + {translatedGearName} + {abilities.map((ability, i) => ( + + ))} + + ); } diff --git a/app/components/Button.tsx b/app/components/Button.tsx index 54cf7bad9..cd9ca2869 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -4,128 +4,128 @@ import clsx from "clsx"; import * as React from "react"; export interface ButtonProps - extends React.ButtonHTMLAttributes { - variant?: - | "success" - | "outlined" - | "outlined-success" - | "destructive" - | "minimal" - | "minimal-success" - | "minimal-destructive"; - size?: "miniscule" | "tiny" | "big"; - loading?: boolean; - loadingText?: string; - icon?: JSX.Element; - testId?: string; - _ref?: React.LegacyRef | React.ForwardedRef; + extends React.ButtonHTMLAttributes { + variant?: + | "success" + | "outlined" + | "outlined-success" + | "destructive" + | "minimal" + | "minimal-success" + | "minimal-destructive"; + size?: "miniscule" | "tiny" | "big"; + loading?: boolean; + loadingText?: string; + icon?: JSX.Element; + testId?: string; + _ref?: React.LegacyRef | React.ForwardedRef; } export function Button(props: ButtonProps) { - const { - variant, - loading, - children, - loadingText, - size, - className, - icon, - type = "button", - testId, - _ref, - ...rest - } = props; - return ( - - ); + const { + variant, + loading, + children, + loadingText, + size, + className, + icon, + type = "button", + testId, + _ref, + ...rest + } = props; + return ( + + ); } type LinkButtonProps = Pick< - ButtonProps, - "variant" | "children" | "className" | "size" | "testId" | "icon" + ButtonProps, + "variant" | "children" | "className" | "size" | "testId" | "icon" > & - Pick & { "data-cy"?: string } & { - isExternal?: boolean; - }; + Pick & { "data-cy"?: string } & { + isExternal?: boolean; + }; export function LinkButton({ - variant, - children, - size, - className, - to, - prefetch, - isExternal, - state, - testId, - icon, + variant, + children, + size, + className, + to, + prefetch, + isExternal, + state, + testId, + icon, }: LinkButtonProps) { - if (isExternal) { - return ( - - {icon && - React.cloneElement(icon, { - className: clsx("button-icon", { - lonely: !children, - }), - })} - {children} - - ); - } + if (isExternal) { + return ( + + {icon && + React.cloneElement(icon, { + className: clsx("button-icon", { + lonely: !children, + }), + })} + {children} + + ); + } - return ( - - {icon && - React.cloneElement(icon, { - className: clsx("button-icon", { lonely: !children }), - })} - {children} - - ); + return ( + + {icon && + React.cloneElement(icon, { + className: clsx("button-icon", { lonely: !children }), + })} + {children} + + ); } diff --git a/app/components/Catcher.tsx b/app/components/Catcher.tsx index e0a94754b..edbfa0bd8 100644 --- a/app/components/Catcher.tsx +++ b/app/components/Catcher.tsx @@ -2,84 +2,84 @@ import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; import { Button } from "~/components/Button"; import { useUser } from "~/features/auth/core/user"; import { - ERROR_GIRL_IMAGE_PATH, - LOG_IN_URL, - SENDOU_INK_DISCORD_URL, + ERROR_GIRL_IMAGE_PATH, + LOG_IN_URL, + SENDOU_INK_DISCORD_URL, } from "~/utils/urls"; import { Image } from "./Image"; import { Main } from "./Main"; export function Catcher() { - const error = useRouteError(); - const user = useUser(); + const error = useRouteError(); + const user = useUser(); - if (!isRouteErrorResponse(error)) - return ( -
- -

Error happened

-

- It seems like you encountered a bug. Sorry about that! Please report - details (your browser? what were you doing?) on{" "} - our Discord so it can be fixed. -

-
- ); + if (!isRouteErrorResponse(error)) + return ( +
+ +

Error happened

+

+ It seems like you encountered a bug. Sorry about that! Please report + details (your browser? what were you doing?) on{" "} + our Discord so it can be fixed. +

+
+ ); - switch (error.status) { - case 401: - return ( -
-

Error 401 Unauthorized

- {user ? ( - - ) : ( -
-

- You should try{" "} - -

-
- )} -
- ); - case 404: - return ( -
-

Error {error.status} - Page not found

- -
- ); - default: - return ( -
-

Error {error.status}

- -
- Please include the message below if any and an explanation on what - you were doing: -
- {error.data ? ( -
{JSON.stringify(JSON.parse(error.data), null, 2)}
- ) : null} -
- ); - } + switch (error.status) { + case 401: + return ( +
+

Error 401 Unauthorized

+ {user ? ( + + ) : ( +
+

+ You should try{" "} + +

+
+ )} +
+ ); + case 404: + return ( +
+

Error {error.status} - Page not found

+ +
+ ); + default: + return ( +
+

Error {error.status}

+ +
+ Please include the message below if any and an explanation on what + you were doing: +
+ {error.data ? ( +
{JSON.stringify(JSON.parse(error.data), null, 2)}
+ ) : null} +
+ ); + } } function GetHelp() { - return ( -

- If you need assistance you can ask for help on{" "} - our Discord -

- ); + return ( +

+ If you need assistance you can ask for help on{" "} + our Discord +

+ ); } diff --git a/app/components/Chart.tsx b/app/components/Chart.tsx index 804a2e2bc..76e3f5504 100644 --- a/app/components/Chart.tsx +++ b/app/components/Chart.tsx @@ -3,156 +3,156 @@ import * as React from "react"; import { type AxisOptions, Chart as ReactChart } from "react-charts"; import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer"; import { useTranslation } from "react-i18next"; -import { useIsMounted } from "~/hooks/useIsMounted"; import { Theme, useTheme } from "~/features/theme/core/provider"; +import { useIsMounted } from "~/hooks/useIsMounted"; export default function Chart({ - options, - containerClassName, - headerSuffix, - valueSuffix, - xAxis, + options, + containerClassName, + headerSuffix, + valueSuffix, + xAxis, }: { - options: [ - { label: string; data: Array<{ primary: Date; secondary: number }> }, - ]; - containerClassName?: string; - headerSuffix?: string; - valueSuffix?: string; - xAxis: "linear" | "localTime"; + options: [ + { label: string; data: Array<{ primary: Date; secondary: number }> }, + ]; + containerClassName?: string; + headerSuffix?: string; + valueSuffix?: string; + xAxis: "linear" | "localTime"; }) { - const { i18n } = useTranslation(); - const theme = useTheme(); - const isMounted = useIsMounted(); + const { i18n } = useTranslation(); + const theme = useTheme(); + const isMounted = useIsMounted(); - const primaryAxis = React.useMemo< - AxisOptions<(typeof options)[number]["data"][number]> - >( - // @ts-expect-error TODO: type this - () => ({ - getValue: (datum) => datum.primary, - scaleType: xAxis, - shouldNice: false, - formatters: { - scale: (val: any) => { - if (val instanceof Date) { - return val.toLocaleDateString(i18n.language, { - day: "numeric", - month: "numeric", - }); - } + const primaryAxis = React.useMemo< + AxisOptions<(typeof options)[number]["data"][number]> + >( + // @ts-expect-error TODO: type this + () => ({ + getValue: (datum) => datum.primary, + scaleType: xAxis, + shouldNice: false, + formatters: { + scale: (val: any) => { + if (val instanceof Date) { + return val.toLocaleDateString(i18n.language, { + day: "numeric", + month: "numeric", + }); + } - return val; - }, - }, - }), - [i18n.language, xAxis], - ); + return val; + }, + }, + }), + [i18n.language, xAxis], + ); - const secondaryAxes = React.useMemo< - AxisOptions<(typeof options)[number]["data"][number]>[] - >( - () => [ - { - getValue: (datum) => datum.secondary, - }, - ], - [], - ); + const secondaryAxes = React.useMemo< + AxisOptions<(typeof options)[number]["data"][number]>[] + >( + () => [ + { + getValue: (datum) => datum.secondary, + }, + ], + [], + ); - if (!isMounted) { - return
; - } + if (!isMounted) { + return
; + } - return ( -
- ( - - ), - }, - primaryCursor: false, - secondaryCursor: false, - primaryAxis, - secondaryAxes, - dark: theme.htmlThemeClass === Theme.DARK, - defaultColors: [ - "var(--theme)", - "var(--theme-secondary)", - "var(--theme-info)", - ], - }} - /> -
- ); + return ( +
+ ( + + ), + }, + primaryCursor: false, + secondaryCursor: false, + primaryAxis, + secondaryAxes, + dark: theme.htmlThemeClass === Theme.DARK, + defaultColors: [ + "var(--theme)", + "var(--theme-secondary)", + "var(--theme-info)", + ], + }} + /> +
+ ); } interface ChartTooltipProps extends TooltipRendererProps { - headerSuffix?: string; - valueSuffix?: string; + headerSuffix?: string; + valueSuffix?: string; } function ChartTooltip({ - focusedDatum, - headerSuffix = "", - valueSuffix = "", + focusedDatum, + headerSuffix = "", + valueSuffix = "", }: ChartTooltipProps) { - const { i18n } = useTranslation(); - const dataPoints = focusedDatum?.interactiveGroup ?? []; + const { i18n } = useTranslation(); + const dataPoints = focusedDatum?.interactiveGroup ?? []; - const header = () => { - const primaryValue = dataPoints[0]?.primaryValue; - if (!primaryValue) return null; + const header = () => { + const primaryValue = dataPoints[0]?.primaryValue; + if (!primaryValue) return null; - if (primaryValue instanceof Date) { - return primaryValue.toLocaleDateString(i18n.language, { - weekday: "short", - day: "numeric", - month: "long", - }); - } + if (primaryValue instanceof Date) { + return primaryValue.toLocaleDateString(i18n.language, { + weekday: "short", + day: "numeric", + month: "long", + }); + } - return primaryValue; - }; + return primaryValue; + }; - return ( -
-

- {header()} - {headerSuffix} -

- {dataPoints.map((dataPoint, index) => { - const color = dataPoint.style?.fill ?? "var(--theme)"; + return ( +
+

+ {header()} + {headerSuffix} +

+ {dataPoints.map((dataPoint, index) => { + const color = dataPoint.style?.fill ?? "var(--theme)"; - return ( -
-
-
- {dataPoint.originalSeries.label} -
-
- {dataPoint.secondaryValue} - {valueSuffix} -
-
- ); - })} -
- ); + return ( +
+
+
+ {dataPoint.originalSeries.label} +
+
+ {dataPoint.secondaryValue} + {valueSuffix} +
+
+ ); + })} +
+ ); } diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx index b6449b2e7..e561f3301 100644 --- a/app/components/Combobox.tsx +++ b/app/components/Combobox.tsx @@ -2,447 +2,447 @@ import { Combobox as HeadlessCombobox } from "@headlessui/react"; import clsx from "clsx"; import Fuse, { type IFuseOptions } from "fuse.js"; import * as React from "react"; -import type { GearType } from "~/db/types"; -import { useAllEventsWithMapPools } from "~/hooks/swr"; import { useTranslation } from "react-i18next"; +import type { GearType } from "~/db/types"; +import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; +import { useAllEventsWithMapPools } from "~/hooks/swr"; import type { MainWeaponId } from "~/modules/in-game-lists"; import { - clothesGearIds, - headGearIds, - mainWeaponIds, - shoesGearIds, - subWeaponIds, - weaponCategories, + clothesGearIds, + headGearIds, + mainWeaponIds, + shoesGearIds, + subWeaponIds, + weaponCategories, } from "~/modules/in-game-lists"; +import { weaponAltNames } from "~/modules/in-game-lists/weapon-alt-names"; import { - nonBombSubWeaponIds, - nonDamagingSpecialWeaponIds, - specialWeaponIds, + nonBombSubWeaponIds, + nonDamagingSpecialWeaponIds, + specialWeaponIds, } from "~/modules/in-game-lists/weapon-ids"; -import { type SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; import type { Unpacked } from "~/utils/types"; import { - gearImageUrl, - mainWeaponImageUrl, - specialWeaponImageUrl, - subWeaponImageUrl, + gearImageUrl, + mainWeaponImageUrl, + specialWeaponImageUrl, + subWeaponImageUrl, } from "~/utils/urls"; import { Image } from "./Image"; -import { weaponAltNames } from "~/modules/in-game-lists/weapon-alt-names"; const MAX_RESULTS_SHOWN = 6; interface ComboboxBaseOption { - label: string; - /** Alternative text other than label to match by */ - alt?: string[]; - value: string; - imgPath?: string; + label: string; + /** Alternative text other than label to match by */ + alt?: string[]; + value: string; + imgPath?: string; } type ComboboxOption = ComboboxBaseOption & T; interface ComboboxProps { - options: ComboboxOption[]; - quickSelectOptions?: ComboboxOption[]; - inputName: string; - placeholder: string; - className?: string; - id?: string; - isLoading?: boolean; - required?: boolean; - value?: ComboboxOption | null; - initialValue: ComboboxOption | null; - onChange?: (selectedOption: ComboboxOption | null) => void; - fullWidth?: boolean; - nullable?: true; - fuseOptions?: IFuseOptions>; + options: ComboboxOption[]; + quickSelectOptions?: ComboboxOption[]; + inputName: string; + placeholder: string; + className?: string; + id?: string; + isLoading?: boolean; + required?: boolean; + value?: ComboboxOption | null; + initialValue: ComboboxOption | null; + onChange?: (selectedOption: ComboboxOption | null) => void; + fullWidth?: boolean; + nullable?: true; + fuseOptions?: IFuseOptions>; } export function Combobox< - T extends Record, + T extends Record, >({ - options, - quickSelectOptions, - inputName, - placeholder, - value, - initialValue, - onChange, - required, - className, - id, - nullable, - isLoading = false, - fullWidth = false, - fuseOptions = {}, + options, + quickSelectOptions, + inputName, + placeholder, + value, + initialValue, + onChange, + required, + className, + id, + nullable, + isLoading = false, + fullWidth = false, + fuseOptions = {}, }: ComboboxProps) { - const { t } = useTranslation(); - const buttonRef = React.useRef(null); - const inputRef = React.useRef(null); + const { t } = useTranslation(); + const buttonRef = React.useRef(null); + const inputRef = React.useRef(null); - const [_selectedOption, setSelectedOption] = React.useState | null>(initialValue); - const [query, setQuery] = React.useState(""); + const [_selectedOption, setSelectedOption] = React.useState | null>(initialValue); + const [query, setQuery] = React.useState(""); - const fuse = new Fuse(options, { - ...fuseOptions, - keys: ["label", "alt"], - }); + const fuse = new Fuse(options, { + ...fuseOptions, + keys: ["label", "alt"], + }); - const filteredOptions = (() => { - if (!query) { - if (quickSelectOptions) return quickSelectOptions; + const filteredOptions = (() => { + if (!query) { + if (quickSelectOptions) return quickSelectOptions; - return []; - } + return []; + } - return fuse - .search(query) - .slice(0, MAX_RESULTS_SHOWN) - .map((res) => res.item); - })(); + return fuse + .search(query) + .slice(0, MAX_RESULTS_SHOWN) + .map((res) => res.item); + })(); - const noMatches = filteredOptions.length === 0; + const noMatches = filteredOptions.length === 0; - const displayValue = (option: Unpacked) => { - return option?.label ?? ""; - }; + const displayValue = (option: Unpacked) => { + return option?.label ?? ""; + }; - const selectedOption = value ?? _selectedOption; + const selectedOption = value ?? _selectedOption; - const showComboboxOptions = () => { - if (!quickSelectOptions || quickSelectOptions.length === 0) return; + const showComboboxOptions = () => { + if (!quickSelectOptions || quickSelectOptions.length === 0) return; - buttonRef.current?.click(); - }; + buttonRef.current?.click(); + }; - return ( -
- { - onChange?.(selected); - setSelectedOption(selected); - // https://github.com/tailwindlabs/headlessui/issues/1555 - // note that this still seems to be a problem despite what the issue says - setTimeout(() => inputRef.current?.blur(), 0); - }} - name={inputName} - disabled={!selectedOption && isLoading} - // TODO: remove hack that prevents TS from freaking out. probably related: https://github.com/tailwindlabs/headlessui/issues/1895 - nullable={nullable as true} - > - setQuery(event.target.value)} - placeholder={isLoading ? t("actions.loading") : placeholder} - className={clsx("combobox-input", className, { - fullWidth, - })} - defaultValue={initialValue} - displayValue={displayValue} - data-testid={`${inputName}-combobox-input`} - id={id} - required={required} - autoComplete="off" - onFocus={showComboboxOptions} - ref={inputRef} - /> - - {isLoading ? ( -
{t("actions.loading")}
- ) : noMatches ? ( -
- {t("forms.errors.noSearchMatches")}{" "} - 🤔 -
- ) : ( - filteredOptions.map((option) => ( - - {({ active }) => ( -
  • - {option.imgPath && ( - - )} - {option.label} -
  • - )} -
    - )) - )} -
    - -
    -
    - ); + return ( +
    + { + onChange?.(selected); + setSelectedOption(selected); + // https://github.com/tailwindlabs/headlessui/issues/1555 + // note that this still seems to be a problem despite what the issue says + setTimeout(() => inputRef.current?.blur(), 0); + }} + name={inputName} + disabled={!selectedOption && isLoading} + // TODO: remove hack that prevents TS from freaking out. probably related: https://github.com/tailwindlabs/headlessui/issues/1895 + nullable={nullable as true} + > + setQuery(event.target.value)} + placeholder={isLoading ? t("actions.loading") : placeholder} + className={clsx("combobox-input", className, { + fullWidth, + })} + defaultValue={initialValue} + displayValue={displayValue} + data-testid={`${inputName}-combobox-input`} + id={id} + required={required} + autoComplete="off" + onFocus={showComboboxOptions} + ref={inputRef} + /> + + {isLoading ? ( +
    {t("actions.loading")}
    + ) : noMatches ? ( +
    + {t("forms.errors.noSearchMatches")}{" "} + 🤔 +
    + ) : ( + filteredOptions.map((option) => ( + + {({ active }) => ( +
  • + {option.imgPath && ( + + )} + {option.label} +
  • + )} +
    + )) + )} +
    + +
    +
    + ); } export function WeaponCombobox({ - id, - required, - className, - inputName, - onChange, - initialWeaponId, - weaponIdsToOmit, - fullWidth, - nullable, - value, - quickSelectWeaponIds, + id, + required, + className, + inputName, + onChange, + initialWeaponId, + weaponIdsToOmit, + fullWidth, + nullable, + value, + quickSelectWeaponIds, }: Pick< - ComboboxProps, - | "inputName" - | "onChange" - | "className" - | "id" - | "required" - | "fullWidth" - | "nullable" + ComboboxProps, + | "inputName" + | "onChange" + | "className" + | "id" + | "required" + | "fullWidth" + | "nullable" > & { - initialWeaponId?: (typeof mainWeaponIds)[number]; - weaponIdsToOmit?: Set; - value?: MainWeaponId | null; - /** Weapons to show when there is focus but no query */ - quickSelectWeaponIds?: MainWeaponId[]; + initialWeaponId?: (typeof mainWeaponIds)[number]; + weaponIdsToOmit?: Set; + value?: MainWeaponId | null; + /** Weapons to show when there is focus but no query */ + quickSelectWeaponIds?: MainWeaponId[]; }) { - const { t, i18n } = useTranslation("weapons"); + const { t, i18n } = useTranslation("weapons"); - const alt = (id: (typeof mainWeaponIds)[number]) => { - const result: string[] = []; + const alt = (id: (typeof mainWeaponIds)[number]) => { + const result: string[] = []; - if (i18n.language !== "en") { - result.push(t(`MAIN_${id}`, { lng: "en" })); - } + if (i18n.language !== "en") { + result.push(t(`MAIN_${id}`, { lng: "en" })); + } - const altNames = weaponAltNames.get(id); - if (typeof altNames === "string") { - result.push(altNames); - } else if (Array.isArray(altNames)) { - result.push(...altNames); - } + const altNames = weaponAltNames.get(id); + if (typeof altNames === "string") { + result.push(altNames); + } else if (Array.isArray(altNames)) { + result.push(...altNames); + } - return result; - }; - const idToWeapon = (id: (typeof mainWeaponIds)[number]) => ({ - value: String(id), - label: t(`MAIN_${id}`), - imgPath: mainWeaponImageUrl(id), - alt: alt(id), - }); + return result; + }; + const idToWeapon = (id: (typeof mainWeaponIds)[number]) => ({ + value: String(id), + label: t(`MAIN_${id}`), + imgPath: mainWeaponImageUrl(id), + alt: alt(id), + }); - const options = mainWeaponIds - .filter((id) => !weaponIdsToOmit?.has(id)) - .map(idToWeapon); + const options = mainWeaponIds + .filter((id) => !weaponIdsToOmit?.has(id)) + .map(idToWeapon); - const quickSelectOptions = quickSelectWeaponIds?.flatMap((weaponId) => { - return options.find((option) => option.value === String(weaponId)) ?? []; - }); + const quickSelectOptions = quickSelectWeaponIds?.flatMap((weaponId) => { + return options.find((option) => option.value === String(weaponId)) ?? []; + }); - return ( - - ); + return ( + + ); } export function AllWeaponCombobox({ - id, - inputName, - onChange, - fullWidth, + id, + inputName, + onChange, + fullWidth, }: Pick< - ComboboxProps, - "inputName" | "onChange" | "id" | "fullWidth" + ComboboxProps, + "inputName" | "onChange" | "id" | "fullWidth" >) { - const { t } = useTranslation("weapons"); + const { t } = useTranslation("weapons"); - const options = () => { - const result: ComboboxProps< - Record - >["options"] = []; + const options = () => { + const result: ComboboxProps< + Record + >["options"] = []; - for (const mainWeaponId of mainWeaponIds) { - result.push({ - value: `MAIN_${mainWeaponId}`, - label: t(`MAIN_${mainWeaponId}`), - imgPath: mainWeaponImageUrl(mainWeaponId), - }); - } + for (const mainWeaponId of mainWeaponIds) { + result.push({ + value: `MAIN_${mainWeaponId}`, + label: t(`MAIN_${mainWeaponId}`), + imgPath: mainWeaponImageUrl(mainWeaponId), + }); + } - for (const subWeaponId of subWeaponIds) { - if (nonBombSubWeaponIds.includes(subWeaponId)) continue; + for (const subWeaponId of subWeaponIds) { + if (nonBombSubWeaponIds.includes(subWeaponId)) continue; - result.push({ - value: `SUB_${subWeaponId}`, - label: t(`SUB_${subWeaponId}`), - imgPath: subWeaponImageUrl(subWeaponId), - }); - } + result.push({ + value: `SUB_${subWeaponId}`, + label: t(`SUB_${subWeaponId}`), + imgPath: subWeaponImageUrl(subWeaponId), + }); + } - for (const specialWeaponId of specialWeaponIds) { - if (nonDamagingSpecialWeaponIds.includes(specialWeaponId)) continue; + for (const specialWeaponId of specialWeaponIds) { + if (nonDamagingSpecialWeaponIds.includes(specialWeaponId)) continue; - result.push({ - value: `SPECIAL_${specialWeaponId}`, - label: t(`SPECIAL_${specialWeaponId}`), - imgPath: specialWeaponImageUrl(specialWeaponId), - }); - } + result.push({ + value: `SPECIAL_${specialWeaponId}`, + label: t(`SPECIAL_${specialWeaponId}`), + imgPath: specialWeaponImageUrl(specialWeaponId), + }); + } - return result; - }; + return result; + }; - return ( - - ); + return ( + + ); } export function GearCombobox({ - id, - required, - className, - inputName, - onChange, - gearType, - initialGearId, + id, + required, + className, + inputName, + onChange, + gearType, + initialGearId, }: Pick< - ComboboxProps, - "inputName" | "onChange" | "className" | "id" | "required" + ComboboxProps, + "inputName" | "onChange" | "className" | "id" | "required" > & { gearType: GearType; initialGearId?: number }) { - const { t } = useTranslation("gear"); + const { t } = useTranslation("gear"); - const translationPrefix = - gearType === "HEAD" ? "H" : gearType === "CLOTHES" ? "C" : "S"; - const ids = - gearType === "HEAD" - ? headGearIds - : gearType === "CLOTHES" - ? clothesGearIds - : shoesGearIds; + const translationPrefix = + gearType === "HEAD" ? "H" : gearType === "CLOTHES" ? "C" : "S"; + const ids = + gearType === "HEAD" + ? headGearIds + : gearType === "CLOTHES" + ? clothesGearIds + : shoesGearIds; - const idToGear = (id: (typeof ids)[number]) => ({ - value: String(id), - label: t(`${translationPrefix}_${id}` as any), - imgPath: gearImageUrl(gearType, id), - }); + const idToGear = (id: (typeof ids)[number]) => ({ + value: String(id), + label: t(`${translationPrefix}_${id}` as any), + imgPath: gearImageUrl(gearType, id), + }); - return ( - - ); + return ( + + ); } const mapPoolEventToOption = ( - e: SerializedMapPoolEvent, + e: SerializedMapPoolEvent, ): ComboboxOption> => ({ - serializedMapPool: e.serializedMapPool, - label: e.name, - value: e.id.toString(), + serializedMapPool: e.serializedMapPool, + label: e.name, + value: e.id.toString(), }); type MapPoolEventsComboboxProps = Pick< - ComboboxProps>, - "inputName" | "className" | "id" | "required" + ComboboxProps>, + "inputName" | "className" | "id" | "required" > & { - initialEvent?: SerializedMapPoolEvent; - onChange: (event: SerializedMapPoolEvent | null) => void; + initialEvent?: SerializedMapPoolEvent; + onChange: (event: SerializedMapPoolEvent | null) => void; }; export function MapPoolEventsCombobox({ - id, - required, - className, - inputName, - onChange, - initialEvent, + id, + required, + className, + inputName, + onChange, + initialEvent, }: MapPoolEventsComboboxProps) { - const { t } = useTranslation(); - const { events, isLoading, isError } = useAllEventsWithMapPools(); + const { t } = useTranslation(); + const { events, isLoading, isError } = useAllEventsWithMapPools(); - const options = React.useMemo( - () => (events ? events.map(mapPoolEventToOption) : []), - [events], - ); + const options = React.useMemo( + () => (events ? events.map(mapPoolEventToOption) : []), + [events], + ); - // this is important so that we don't trigger the reset to the initialEvent every time - const initialOption = React.useMemo( - () => initialEvent && mapPoolEventToOption(initialEvent), - [initialEvent], - ); + // this is important so that we don't trigger the reset to the initialEvent every time + const initialOption = React.useMemo( + () => initialEvent && mapPoolEventToOption(initialEvent), + [initialEvent], + ); - if (isError) { - return ( -
    {t("errors.genericReload")}
    - ); - } + if (isError) { + return ( +
    {t("errors.genericReload")}
    + ); + } - return ( - { - onChange( - e && { - id: parseInt(e.value, 10), - name: e.label, - serializedMapPool: e.serializedMapPool, - }, - ); - }} - className={className} - id={id} - required={required} - isLoading={isLoading} - fullWidth - /> - ); + return ( + { + onChange( + e && { + id: Number.parseInt(e.value, 10), + name: e.label, + serializedMapPool: e.serializedMapPool, + }, + ); + }} + className={className} + id={id} + required={required} + isLoading={isLoading} + fullWidth + /> + ); } diff --git a/app/components/ConditionalScrollRestoration.tsx b/app/components/ConditionalScrollRestoration.tsx index d3bf6ae9f..66d8e2d6f 100644 --- a/app/components/ConditionalScrollRestoration.tsx +++ b/app/components/ConditionalScrollRestoration.tsx @@ -5,21 +5,21 @@ import { ScrollRestoration, useLocation } from "@remix-run/react"; import * as React from "react"; export function ConditionalScrollRestoration() { - const isFirstRenderRef = React.useRef(true); - const location = useLocation(); + const isFirstRenderRef = React.useRef(true); + const location = useLocation(); - React.useEffect(() => { - isFirstRenderRef.current = false; - }, []); + React.useEffect(() => { + isFirstRenderRef.current = false; + }, []); - if ( - !isFirstRenderRef.current && - location.state != null && - typeof location.state === "object" && - (location.state as { scroll: boolean }).scroll === false - ) { - return null; - } + if ( + !isFirstRenderRef.current && + location.state != null && + typeof location.state === "object" && + (location.state as { scroll: boolean }).scroll === false + ) { + return null; + } - return location.pathname} />; + return location.pathname} />; } diff --git a/app/components/CustomizedColorsInput.tsx b/app/components/CustomizedColorsInput.tsx index 8514623dd..c0977f436 100644 --- a/app/components/CustomizedColorsInput.tsx +++ b/app/components/CustomizedColorsInput.tsx @@ -1,71 +1,73 @@ -import { useTranslation } from "react-i18next"; -import { Label } from "./Label"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "./Button"; +import { Label } from "./Label"; const CUSTOM_COLORS = [ - "bg", - "bg-darker", - "bg-lighter", - "text", - "text-lighter", - "theme", - "chat", + "bg", + "bg-darker", + "bg-lighter", + "text", + "text-lighter", + "theme", + "chat", ] as const; type CustomColorsRecord = Partial< - Record<(typeof CUSTOM_COLORS)[number], string> + Record<(typeof CUSTOM_COLORS)[number], string> >; export function CustomizedColorsInput({ - initialColors, + initialColors, }: { - initialColors?: Record | null; + initialColors?: Record | null; }) { - const { t } = useTranslation(); - const [colors, setColors] = React.useState( - initialColors ?? {}, - ); + const { t } = useTranslation(); + const [colors, setColors] = React.useState( + initialColors ?? {}, + ); - return ( -
    - - -
    - {CUSTOM_COLORS.map((cssVar) => { - return ( - -
    {t(`custom.colors.${cssVar}`)}
    - { - const extras: Record = {}; - if (cssVar === "bg-lighter") { - extras["bg-lightest"] = `${e.target.value}80`; - } - setColors({ ...colors, ...extras, [cssVar]: e.target.value }); - }} - data-testid={`color-input-${cssVar}`} - /> - -
    - ); - })} -
    -
    - ); + return ( +
    + + +
    + {CUSTOM_COLORS.map((cssVar) => { + return ( + +
    {t(`custom.colors.${cssVar}`)}
    + { + const extras: Record = {}; + if (cssVar === "bg-lighter") { + extras["bg-lightest"] = `${e.target.value}80`; + } + setColors({ ...colors, ...extras, [cssVar]: e.target.value }); + }} + data-testid={`color-input-${cssVar}`} + /> + +
    + ); + })} +
    +
    + ); } diff --git a/app/components/DateInput.tsx b/app/components/DateInput.tsx index c5288c288..eecce225e 100644 --- a/app/components/DateInput.tsx +++ b/app/components/DateInput.tsx @@ -1,70 +1,70 @@ +import * as React from "react"; import { useIsMounted } from "~/hooks/useIsMounted"; import { dateToYearMonthDayHourMinuteString, isValidDate } from "~/utils/dates"; -import * as React from "react"; export interface DateInputProps - extends Omit< - React.InputHTMLAttributes, - "defaultValue" | "min" | "max" | "onChange" | "value" - > { - defaultValue?: Date; - min?: Date; - max?: Date; - onChange?: (newDate: Date | null) => void; + extends Omit< + React.InputHTMLAttributes, + "defaultValue" | "min" | "max" | "onChange" | "value" + > { + defaultValue?: Date; + min?: Date; + max?: Date; + onChange?: (newDate: Date | null) => void; } export function DateInput({ - name, - defaultValue, - min, - max, - onChange, - ...inputProps + name, + defaultValue, + min, + max, + onChange, + ...inputProps }: DateInputProps) { - // Keeping track of the value as a string is a nice fallback for browsers that - // don't show a date picker but actually expect the user to type in the date - // as a text. This was Safari Desktop until recently, but nowadays all current - // versions of the main browsers set the input to either a valid date string - // or "". (The browser will handle transitional invalid states internally). - const [[parsedDate, valueString], setDate] = React.useState< - [Date | null, string] - >(() => { - if (defaultValue) { - if (isValidDate(defaultValue)) { - return [defaultValue, dateToYearMonthDayHourMinuteString(defaultValue)]; - } - console.warn("DateInput got invalid date as defaultValue"); - } - return [null, ""]; - }); - const isMounted = useIsMounted(); + // Keeping track of the value as a string is a nice fallback for browsers that + // don't show a date picker but actually expect the user to type in the date + // as a text. This was Safari Desktop until recently, but nowadays all current + // versions of the main browsers set the input to either a valid date string + // or "". (The browser will handle transitional invalid states internally). + const [[parsedDate, valueString], setDate] = React.useState< + [Date | null, string] + >(() => { + if (defaultValue) { + if (isValidDate(defaultValue)) { + return [defaultValue, dateToYearMonthDayHourMinuteString(defaultValue)]; + } + console.warn("DateInput got invalid date as defaultValue"); + } + return [null, ""]; + }); + const isMounted = useIsMounted(); - return ( - <> - {parsedDate && isMounted && ( - - )} - { - const newValueString = e.target.value; - const parsedValue = new Date(newValueString); - const newDate = isValidDate(parsedValue) ? parsedValue : null; + return ( + <> + {parsedDate && isMounted && ( + + )} + { + const newValueString = e.target.value; + const parsedValue = new Date(newValueString); + const newDate = isValidDate(parsedValue) ? parsedValue : null; - setDate([newDate, newValueString]); - onChange?.(newDate); - }} - // Firefox fix for hydration error "prop `disabled` did not match" */ - // https://github.com/facebook/react/issues/21459 - autoComplete="off" - /> - - ); + setDate([newDate, newValueString]); + onChange?.(newDate); + }} + // Firefox fix for hydration error "prop `disabled` did not match" */ + // https://github.com/facebook/react/issues/21459 + autoComplete="off" + /> + + ); } diff --git a/app/components/DetailsSummary.tsx b/app/components/DetailsSummary.tsx index a16dd9c4c..5ff7ddafb 100644 --- a/app/components/DetailsSummary.tsx +++ b/app/components/DetailsSummary.tsx @@ -2,21 +2,21 @@ import clsx from "clsx"; import type * as React from "react"; export function Details({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) { - return
    {children}
    ; + return
    {children}
    ; } export function Summary({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) { - return {children}; + return {children}; } diff --git a/app/components/Dialog.tsx b/app/components/Dialog.tsx index 7ee777310..f7f12c367 100644 --- a/app/components/Dialog.tsx +++ b/app/components/Dialog.tsx @@ -2,108 +2,107 @@ import React from "react"; import invariant from "~/utils/invariant"; export function Dialog({ - children, - isOpen, - close, - className, - closeOnAnyClick, + children, + isOpen, + close, + className, + closeOnAnyClick, }: { - children: React.ReactNode; - isOpen: boolean; - close?: () => void; - className?: string; - closeOnAnyClick?: boolean; + children: React.ReactNode; + isOpen: boolean; + close?: () => void; + className?: string; + closeOnAnyClick?: boolean; }) { - const ref = useDOMSync(isOpen); - useControlledEsc({ ref, isOpen, close }); + const ref = useDOMSync(isOpen); + useControlledEsc({ ref, isOpen, close }); - // https://stackoverflow.com/a/26984690 - const closeOnOutsideClick = close - ? (event: React.MouseEvent) => { - if (closeOnAnyClick) return close(); - const rect: DOMRect = ref.current.getBoundingClientRect(); - const isInDialog = - rect.top <= event.clientY && - event.clientY <= rect.top + rect.height && - rect.left <= event.clientX && - event.clientX <= rect.left + rect.width; - if (!isInDialog) { - close(); - } - } - : undefined; + // https://stackoverflow.com/a/26984690 + const closeOnOutsideClick = close + ? (event: React.MouseEvent) => { + if (closeOnAnyClick) return close(); + const rect: DOMRect = ref.current.getBoundingClientRect(); + const isInDialog = + rect.top <= event.clientY && + event.clientY <= rect.top + rect.height && + rect.left <= event.clientX && + event.clientX <= rect.left + rect.width; + if (!isInDialog) { + close(); + } + } + : undefined; - return ( - - {children} - - ); + return ( + + {children} + + ); } function useDOMSync(isOpen: boolean) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ref = React.useRef(null); + const ref = React.useRef(null); - React.useEffect(() => { - const dialog = ref.current; + React.useEffect(() => { + const dialog = ref.current; - if (dialog.open && isOpen) return; - if (!dialog.open && !isOpen) return; + if (dialog.open && isOpen) return; + if (!dialog.open && !isOpen) return; - const html = document.getElementsByTagName("html")[0]; - invariant(html); + const html = document.getElementsByTagName("html")[0]; + invariant(html); - if (isOpen) { - dialog.showModal(); - // TODO: can be replaced with https://twitter.com/argyleink/status/1529869352660439048 once gets control - html.classList.add("lock-scroll"); - } else { - dialog.close(); - html.classList.remove("lock-scroll"); - } + if (isOpen) { + dialog.showModal(); + // TODO: can be replaced with https://twitter.com/argyleink/status/1529869352660439048 once gets control + html.classList.add("lock-scroll"); + } else { + dialog.close(); + html.classList.remove("lock-scroll"); + } - return () => { - dialog.close(); - html.classList.remove("lock-scroll"); - }; - }, [isOpen]); + return () => { + dialog.close(); + html.classList.remove("lock-scroll"); + }; + }, [isOpen]); - return ref; + return ref; } function useControlledEsc({ - ref, - isOpen, - close, + ref, + isOpen, + close, }: { - ref: React.MutableRefObject; - isOpen: boolean; - close?: () => void; + ref: React.MutableRefObject; + isOpen: boolean; + close?: () => void; }) { - React.useEffect(() => { - const dialog = ref.current; - if (!dialog) return; + React.useEffect(() => { + const dialog = ref.current; + if (!dialog) return; - const preventDefault = (event: KeyboardEvent) => { - event.preventDefault(); - }; - dialog.addEventListener("cancel", preventDefault); + const preventDefault = (event: KeyboardEvent) => { + event.preventDefault(); + }; + dialog.addEventListener("cancel", preventDefault); - return () => { - dialog.removeEventListener("cancel", preventDefault); - }; - }, [ref]); + return () => { + dialog.removeEventListener("cancel", preventDefault); + }; + }, [ref]); - React.useEffect(() => { - if (!isOpen || !close) return; + React.useEffect(() => { + if (!isOpen || !close) return; - const closeOnEsc = (event: KeyboardEvent) => { - if (event.key === "Escape") { - close(); - } - }; + const closeOnEsc = (event: KeyboardEvent) => { + if (event.key === "Escape") { + close(); + } + }; - document.addEventListener("keydown", closeOnEsc); - return () => document.removeEventListener("keydown", closeOnEsc); - }, [isOpen, close]); + document.addEventListener("keydown", closeOnEsc); + return () => document.removeEventListener("keydown", closeOnEsc); + }, [isOpen, close]); } diff --git a/app/components/Divider.tsx b/app/components/Divider.tsx index 98001c548..d4d49bbf2 100644 --- a/app/components/Divider.tsx +++ b/app/components/Divider.tsx @@ -1,17 +1,17 @@ import clsx from "clsx"; export function Divider({ - children, - className, - smallText, + children, + className, + smallText, }: { - children?: React.ReactNode; - className?: string; - smallText?: boolean; + children?: React.ReactNode; + className?: string; + smallText?: boolean; }) { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); } diff --git a/app/components/Draggable.tsx b/app/components/Draggable.tsx index 1c16c5563..e1b7f4e45 100644 --- a/app/components/Draggable.tsx +++ b/app/components/Draggable.tsx @@ -3,33 +3,33 @@ import { CSS } from "@dnd-kit/utilities"; import type * as React from "react"; export function Draggable({ - id, - disabled, - liClassName, - children, + id, + disabled, + liClassName, + children, }: { - id: number; - disabled: boolean; - liClassName: string; - children: React.ReactNode; + id: number; + disabled: boolean; + liClassName: string; + children: React.ReactNode; }) { - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id, disabled }); + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id, disabled }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; - return ( -
  • - {children} -
  • - ); + return ( +
  • + {children} +
  • + ); } diff --git a/app/components/Flag.tsx b/app/components/Flag.tsx index 3ff9b4712..792d7ea5a 100644 --- a/app/components/Flag.tsx +++ b/app/components/Flag.tsx @@ -1,18 +1,18 @@ import clsx from "clsx"; export function Flag({ - countryCode, - tiny = false, + countryCode, + tiny = false, }: { - countryCode: string; - tiny?: boolean; + countryCode: string; + tiny?: boolean; }) { - return ( -
    - ); + return ( +
    + ); } diff --git a/app/components/FormErrors.tsx b/app/components/FormErrors.tsx index a397eb068..f4661b4cf 100644 --- a/app/components/FormErrors.tsx +++ b/app/components/FormErrors.tsx @@ -3,25 +3,25 @@ import type { CustomTypeOptions } from "react-i18next"; import { useTranslation } from "react-i18next"; export function FormErrors({ - namespace, + namespace, }: { - namespace: keyof CustomTypeOptions["resources"]; + namespace: keyof CustomTypeOptions["resources"]; }) { - const { t } = useTranslation(["common", namespace]); - const actionData = useActionData<{ errors?: string[] }>(); + const { t } = useTranslation(["common", namespace]); + const actionData = useActionData<{ errors?: string[] }>(); - if (!actionData?.errors || actionData.errors.length === 0) { - return null; - } + if (!actionData?.errors || actionData.errors.length === 0) { + return null; + } - return ( -
    -

    {t("common:forms.errors.title")}:

    -
      - {actionData.errors.map((error) => ( -
    1. {t(`${namespace}:${error}` as any)}
    2. - ))} -
    -
    - ); + return ( +
    +

    {t("common:forms.errors.title")}:

    +
      + {actionData.errors.map((error) => ( +
    1. {t(`${namespace}:${error}` as any)}
    2. + ))} +
    +
    + ); } diff --git a/app/components/FormMessage.tsx b/app/components/FormMessage.tsx index 53cf27b0d..4e90444f0 100644 --- a/app/components/FormMessage.tsx +++ b/app/components/FormMessage.tsx @@ -2,22 +2,22 @@ import clsx from "clsx"; import type * as React from "react"; export function FormMessage({ - children, - type, - className, + children, + type, + className, }: { - children: React.ReactNode; - type: "error" | "info"; - className?: string; + children: React.ReactNode; + type: "error" | "info"; + className?: string; }) { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); } diff --git a/app/components/FormWithConfirm.tsx b/app/components/FormWithConfirm.tsx index ffb11f711..342e958c8 100644 --- a/app/components/FormWithConfirm.tsx +++ b/app/components/FormWithConfirm.tsx @@ -1,98 +1,98 @@ import { type FetcherWithComponents, useFetcher } from "@remix-run/react"; import * as React from "react"; -import invariant from "~/utils/invariant"; +import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; +import { useIsMounted } from "~/hooks/useIsMounted"; +import invariant from "~/utils/invariant"; import { Button, type ButtonProps } from "./Button"; import { Dialog } from "./Dialog"; import { SubmitButton } from "./SubmitButton"; -import { useIsMounted } from "~/hooks/useIsMounted"; -import { createPortal } from "react-dom"; export function FormWithConfirm({ - fields, - children, - dialogHeading, - deleteButtonText, - cancelButtonText, - action, - submitButtonTestId = "submit-button", - submitButtonVariant = "destructive", - fetcher: _fetcher, + fields, + children, + dialogHeading, + deleteButtonText, + cancelButtonText, + action, + submitButtonTestId = "submit-button", + submitButtonVariant = "destructive", + fetcher: _fetcher, }: { - fields?: ( - | [name: string, value: string | number] - | readonly [name: string, value: string | number] - )[]; - children: React.ReactNode; - dialogHeading: string; - deleteButtonText?: string; - cancelButtonText?: string; - action?: string; - submitButtonTestId?: string; - submitButtonVariant?: ButtonProps["variant"]; - fetcher?: FetcherWithComponents; + fields?: ( + | [name: string, value: string | number] + | readonly [name: string, value: string | number] + )[]; + children: React.ReactNode; + dialogHeading: string; + deleteButtonText?: string; + cancelButtonText?: string; + action?: string; + submitButtonTestId?: string; + submitButtonVariant?: ButtonProps["variant"]; + fetcher?: FetcherWithComponents; }) { - const componentsFetcher = useFetcher(); - const fetcher = _fetcher ?? componentsFetcher; + const componentsFetcher = useFetcher(); + const fetcher = _fetcher ?? componentsFetcher; - const isMounted = useIsMounted(); - const { t } = useTranslation(["common"]); - const [dialogOpen, setDialogOpen] = React.useState(false); - const formRef = React.useRef(null); - const id = React.useId(); + const isMounted = useIsMounted(); + const { t } = useTranslation(["common"]); + const [dialogOpen, setDialogOpen] = React.useState(false); + const formRef = React.useRef(null); + const id = React.useId(); - const openDialog = () => setDialogOpen(true); - const closeDialog = () => setDialogOpen(false); + const openDialog = React.useCallback(() => setDialogOpen(true), []); + const closeDialog = React.useCallback(() => setDialogOpen(false), []); - invariant(React.isValidElement(children)); + invariant(React.isValidElement(children)); - React.useEffect(() => { - if (fetcher.state === "loading") { - closeDialog(); - } - }, [fetcher.state]); + React.useEffect(() => { + if (fetcher.state === "loading") { + closeDialog(); + } + }, [fetcher.state, closeDialog]); - return ( - <> - {isMounted - ? // using portal here makes nesting this component in another form work - createPortal( - - {fields?.map(([name, value]) => ( - - ))} - , - document.body, - ) - : null} - -
    -

    {dialogHeading}

    -
    - - {deleteButtonText ?? t("common:actions.delete")} - - -
    -
    -
    - {React.cloneElement(children, { - // @ts-expect-error broke with @types/react upgrade. TODO: figure out narrower type than React.ReactNode - onClick: openDialog, - type: "button", - })} - - ); + return ( + <> + {isMounted + ? // using portal here makes nesting this component in another form work + createPortal( + + {fields?.map(([name, value]) => ( + + ))} + , + document.body, + ) + : null} + +
    +

    {dialogHeading}

    +
    + + {deleteButtonText ?? t("common:actions.delete")} + + +
    +
    +
    + {React.cloneElement(children, { + // @ts-expect-error broke with @types/react upgrade. TODO: figure out narrower type than React.ReactNode + onClick: openDialog, + type: "button", + })} + + ); } diff --git a/app/components/FriendCodeInput.tsx b/app/components/FriendCodeInput.tsx index 71e96a1fa..82c28be36 100644 --- a/app/components/FriendCodeInput.tsx +++ b/app/components/FriendCodeInput.tsx @@ -8,42 +8,42 @@ import { FRIEND_CODE_REGEXP_PATTERN } from "~/features/sendouq/q-constants"; import { SENDOUQ_PAGE } from "~/utils/urls"; export function FriendCodeInput({ - friendCode, + friendCode, }: { - friendCode?: string | null; + friendCode?: string | null; }) { - const fetcher = useFetcher(); - const { t } = useTranslation(["common"]); + const fetcher = useFetcher(); + const { t } = useTranslation(["common"]); - return ( - -
    -
    - {!friendCode ? ( - - ) : null} - {friendCode ? ( -
    SW-{friendCode}
    - ) : ( - - )} -
    - {!friendCode ? ( - - Save - - ) : null} -
    -
    - ); + return ( + +
    +
    + {!friendCode ? ( + + ) : null} + {friendCode ? ( +
    SW-{friendCode}
    + ) : ( + + )} +
    + {!friendCode ? ( + + Save + + ) : null} +
    +
    + ); } diff --git a/app/components/Image.tsx b/app/components/Image.tsx index 4236f343b..85867e46f 100644 --- a/app/components/Image.tsx +++ b/app/components/Image.tsx @@ -1,174 +1,174 @@ -import type { TierName } from "~/features/mmr/mmr-constants"; +import clsx from "clsx"; import { useTranslation } from "react-i18next"; +import type { TierName } from "~/features/mmr/mmr-constants"; import type { MainWeaponId, ModeShort, StageId } from "~/modules/in-game-lists"; import { - TIER_PLUS_URL, - mainWeaponImageUrl, - modeImageUrl, - outlinedFiveStarMainWeaponImageUrl, - outlinedMainWeaponImageUrl, - stageImageUrl, - tierImageUrl, + TIER_PLUS_URL, + mainWeaponImageUrl, + modeImageUrl, + outlinedFiveStarMainWeaponImageUrl, + outlinedMainWeaponImageUrl, + stageImageUrl, + tierImageUrl, } from "~/utils/urls"; -import clsx from "clsx"; interface ImageProps { - path: string; - alt: string; - title?: string; - className?: string; - containerClassName?: string; - width?: number; - height?: number; - size?: number; - style?: React.CSSProperties; - containerStyle?: React.CSSProperties; - testId?: string; - onClick?: () => void; - loading?: "lazy"; + path: string; + alt: string; + title?: string; + className?: string; + containerClassName?: string; + width?: number; + height?: number; + size?: number; + style?: React.CSSProperties; + containerStyle?: React.CSSProperties; + testId?: string; + onClick?: () => void; + loading?: "lazy"; } export function Image({ - path, - alt, - title, - className, - width, - height, - size, - style, - testId, - containerClassName, - containerStyle, - onClick, - loading, + path, + alt, + title, + className, + width, + height, + size, + style, + testId, + containerClassName, + containerStyle, + onClick, + loading, }: ImageProps) { - return ( - - - {alt} - - ); + return ( + + + {alt} + + ); } type WeaponImageProps = { - weaponSplId: MainWeaponId; - variant: "badge" | "badge-5-star" | "build"; + weaponSplId: MainWeaponId; + variant: "badge" | "badge-5-star" | "build"; } & Omit; export function WeaponImage({ - weaponSplId, - variant, - testId, - title, - ...rest + weaponSplId, + variant, + testId, + title, + ...rest }: WeaponImageProps) { - const { t } = useTranslation(["weapons"]); + const { t } = useTranslation(["weapons"]); - return ( - {title - ); + return ( + {title + ); } type ModeImageProps = { - mode: ModeShort; + mode: ModeShort; } & Omit; export function ModeImage({ mode, testId, title, ...rest }: ModeImageProps) { - const { t } = useTranslation(["game-misc"]); + const { t } = useTranslation(["game-misc"]); - return ( - {title - ); + return ( + {title + ); } type StageImageProps = { - stageId: StageId; + stageId: StageId; } & Omit; export function StageImage({ stageId, testId, ...rest }: StageImageProps) { - const { t } = useTranslation(["game-misc"]); + const { t } = useTranslation(["game-misc"]); - return ( - {t(`game-misc:STAGE_${stageId}`)} - ); + return ( + {t(`game-misc:STAGE_${stageId}`)} + ); } type TierImageProps = { - tier: { name: TierName; isPlus: boolean }; + tier: { name: TierName; isPlus: boolean }; } & Omit; export function TierImage({ tier, className, width = 200 }: TierImageProps) { - const title = `${tier.name}${tier.isPlus ? "+" : ""}`; + const title = `${tier.name}${tier.isPlus ? "+" : ""}`; - const height = width * 0.8675; + const height = width * 0.8675; - return ( -
    - {title} - {tier.isPlus ? ( - {title} - ) : null} -
    - ); + return ( +
    + {title} + {tier.isPlus ? ( + {title} + ) : null} +
    + ); } diff --git a/app/components/InfoPopover.tsx b/app/components/InfoPopover.tsx index fdcb89feb..29d6431b4 100644 --- a/app/components/InfoPopover.tsx +++ b/app/components/InfoPopover.tsx @@ -1,9 +1,9 @@ import { Popover } from "./Popover"; export function InfoPopover({ children }: { children: React.ReactNode }) { - return ( - ?} triggerClassName="info-popover__trigger"> - {children} - - ); + return ( + ?} triggerClassName="info-popover__trigger"> + {children} + + ); } diff --git a/app/components/Input.tsx b/app/components/Input.tsx index d7b334a30..d0b219b2a 100644 --- a/app/components/Input.tsx +++ b/app/components/Input.tsx @@ -1,78 +1,78 @@ import clsx from "clsx"; export function Input({ - name, - id, - className, - minLength, - maxLength, - required, - defaultValue, - leftAddon, - icon, - type, - min, - max, - pattern, - list, - testId, - "aria-label": ariaLabel, - value, - placeholder, - onChange, - disableAutoComplete = false, - readOnly, + name, + id, + className, + minLength, + maxLength, + required, + defaultValue, + leftAddon, + icon, + type, + min, + max, + pattern, + list, + testId, + "aria-label": ariaLabel, + value, + placeholder, + onChange, + disableAutoComplete = false, + readOnly, }: { - name?: string; - id?: string; - className?: string; - minLength?: number; - maxLength?: number; - required?: boolean; - defaultValue?: string; - leftAddon?: string; - icon?: React.ReactNode; - type?: "number" | "date"; - min?: number; - max?: number | string; - pattern?: string; - list?: string; - testId?: string; - "aria-label"?: string; - value?: string; - placeholder?: string; - onChange?: (e: React.ChangeEvent) => void; - disableAutoComplete?: boolean; - readOnly?: boolean; + name?: string; + id?: string; + className?: string; + minLength?: number; + maxLength?: number; + required?: boolean; + defaultValue?: string; + leftAddon?: string; + icon?: React.ReactNode; + type?: "number" | "date"; + min?: number; + max?: number | string; + pattern?: string; + list?: string; + testId?: string; + "aria-label"?: string; + value?: string; + placeholder?: string; + onChange?: (e: React.ChangeEvent) => void; + disableAutoComplete?: boolean; + readOnly?: boolean; }) { - return ( -
    - {leftAddon ?
    {leftAddon}
    : null} - - {icon} -
    - ); + return ( +
    + {leftAddon ?
    {leftAddon}
    : null} + + {icon} +
    + ); } diff --git a/app/components/Label.tsx b/app/components/Label.tsx index f5c430a49..fc13eedfb 100644 --- a/app/components/Label.tsx +++ b/app/components/Label.tsx @@ -1,48 +1,48 @@ import clsx from "clsx"; type LabelProps = Pick< - React.DetailedHTMLProps< - React.LabelHTMLAttributes, - HTMLLabelElement - >, - "children" | "htmlFor" + React.DetailedHTMLProps< + React.LabelHTMLAttributes, + HTMLLabelElement + >, + "children" | "htmlFor" > & { - valueLimits?: { - current: number; - max: number; - }; - required?: boolean; - className?: string; - labelClassName?: string; - spaced?: boolean; + valueLimits?: { + current: number; + max: number; + }; + required?: boolean; + className?: string; + labelClassName?: string; + spaced?: boolean; }; export function Label({ - valueLimits, - required, - children, - htmlFor, - className, - labelClassName, - spaced = true, + valueLimits, + required, + children, + htmlFor, + className, + labelClassName, + spaced = true, }: LabelProps) { - return ( -
    - - {valueLimits ? ( -
    - {valueLimits.current}/{valueLimits.max} -
    - ) : null} -
    - ); + return ( +
    + + {valueLimits ? ( +
    + {valueLimits.current}/{valueLimits.max} +
    + ) : null} +
    + ); } function lengthWarning(valueLimits: NonNullable) { - if (valueLimits.current >= valueLimits.max) return "error"; - if (valueLimits.current / valueLimits.max >= 0.9) return "warning"; + if (valueLimits.current >= valueLimits.max) return "error"; + if (valueLimits.current / valueLimits.max >= 0.9) return "warning"; - return; + return; } diff --git a/app/components/Main.tsx b/app/components/Main.tsx index b0dde777b..afa3869c9 100644 --- a/app/components/Main.tsx +++ b/app/components/Main.tsx @@ -1,64 +1,64 @@ import { - useLocation, - useRouteError, - isRouteErrorResponse, + isRouteErrorResponse, + useLocation, + useRouteError, } from "@remix-run/react"; +import { SideNav } from "app/components/layout/SideNav"; import clsx from "clsx"; import type * as React from "react"; import { useMatches } from "react-router"; import { useUser } from "~/features/auth/core/user"; import type { RootLoaderData } from "~/root"; -import { SideNav } from "app/components/layout/SideNav"; export const Main = ({ - children, - className, - classNameOverwrite, - halfWidth, - bigger, - style, + children, + className, + classNameOverwrite, + halfWidth, + bigger, + style, }: { - children: React.ReactNode; - className?: string; - classNameOverwrite?: string; - halfWidth?: boolean; - bigger?: boolean; - style?: React.CSSProperties; + children: React.ReactNode; + className?: string; + classNameOverwrite?: string; + halfWidth?: boolean; + bigger?: boolean; + style?: React.CSSProperties; }) => { - const error = useRouteError(); - const data = useMatches()[0]?.data as RootLoaderData | undefined; - const user = useUser(); - const showLeaderboard = - data?.publisherId && !user?.patronTier && !isRouteErrorResponse(error); + const error = useRouteError(); + const data = useMatches()[0]?.data as RootLoaderData | undefined; + const user = useUser(); + const showLeaderboard = + data?.publisherId && !user?.patronTier && !isRouteErrorResponse(error); - const location = useLocation(); - const isFrontPage = location.pathname === "/"; + const location = useLocation(); + const isFrontPage = location.pathname === "/"; - return ( -
    - {!isFrontPage ? : null} -
    - {children} -
    -
    - ); + return ( +
    + {!isFrontPage ? : null} +
    + {children} +
    +
    + ); }; diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index b3c4b2f5b..a4a8e469f 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -1,356 +1,356 @@ import clsx from "clsx"; -import { useTranslation } from "react-i18next"; -import { Image } from "~/components/Image"; -import { - type ModeShort, - modesShort, - type StageId, -} from "~/modules/in-game-lists"; -import { modes, stageIds } from "~/modules/in-game-lists"; -import { MapPool } from "~/features/map-list-generator/core/map-pool"; -import { modeImageUrl, stageImageUrl } from "~/utils/urls"; -import { Button } from "~/components/Button"; -import { split, startsWith } from "~/utils/strings"; -import { CrossIcon } from "./icons/Cross"; -import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "~/components/Button"; +import { Image } from "~/components/Image"; import type { CalendarEvent } from "~/db/types"; import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; -import { assertType } from "~/utils/types"; -import { MapPoolEventsCombobox } from "./Combobox"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; +import { + type ModeShort, + type StageId, + modesShort, +} from "~/modules/in-game-lists"; +import { modes, stageIds } from "~/modules/in-game-lists"; +import { split, startsWith } from "~/utils/strings"; +import { assertType } from "~/utils/types"; +import { modeImageUrl, stageImageUrl } from "~/utils/urls"; +import { MapPoolEventsCombobox } from "./Combobox"; +import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; +import { CrossIcon } from "./icons/Cross"; export type MapPoolSelectorProps = { - mapPool: MapPool; - preselectedMapPool?: MapPool; - handleRemoval?: () => void; - handleMapPoolChange: ( - mapPool: MapPool, - event?: Pick, - ) => void; - className?: string; - recentEvents?: SerializedMapPoolEvent[]; - initialEvent?: Pick; - title?: string; - modesToInclude?: ModeShort[]; - info?: React.ReactNode; - footer?: React.ReactNode; - /** Enables clear button, template selection, and toggling a whole stage */ - allowBulkEdit?: boolean; - hideBanned?: boolean; + mapPool: MapPool; + preselectedMapPool?: MapPool; + handleRemoval?: () => void; + handleMapPoolChange: ( + mapPool: MapPool, + event?: Pick, + ) => void; + className?: string; + recentEvents?: SerializedMapPoolEvent[]; + initialEvent?: Pick; + title?: string; + modesToInclude?: ModeShort[]; + info?: React.ReactNode; + footer?: React.ReactNode; + /** Enables clear button, template selection, and toggling a whole stage */ + allowBulkEdit?: boolean; + hideBanned?: boolean; }; export function MapPoolSelector({ - mapPool, - preselectedMapPool, - handleMapPoolChange, - handleRemoval, - className, - recentEvents, - initialEvent, - title, - modesToInclude, - info, - footer, - allowBulkEdit = false, - hideBanned = false, + mapPool, + preselectedMapPool, + handleMapPoolChange, + handleRemoval, + className, + recentEvents, + initialEvent, + title, + modesToInclude, + info, + footer, + allowBulkEdit = false, + hideBanned = false, }: MapPoolSelectorProps) { - const { t } = useTranslation(); + const { t } = useTranslation(); - const [template, setTemplate] = React.useState( - initialEvent ? "event" : detectTemplate(mapPool), - ); + const [template, setTemplate] = React.useState( + initialEvent ? "event" : detectTemplate(mapPool), + ); - const [initialSerializedEvent, setInitialSerializedEvent] = React.useState( - (): SerializedMapPoolEvent | undefined => - initialEvent && { - ...initialEvent, - serializedMapPool: mapPool.serialized, - }, - ); + const [initialSerializedEvent, setInitialSerializedEvent] = React.useState( + (): SerializedMapPoolEvent | undefined => + initialEvent && { + ...initialEvent, + serializedMapPool: mapPool.serialized, + }, + ); - const handleStageModesChange = (newMapPool: MapPool) => { - setTemplate(detectTemplate(newMapPool)); - handleMapPoolChange(newMapPool); - }; + const handleStageModesChange = (newMapPool: MapPool) => { + setTemplate(detectTemplate(newMapPool)); + handleMapPoolChange(newMapPool); + }; - const handleClear = () => { - setTemplate("none"); - handleMapPoolChange(MapPool.EMPTY); - }; + const handleClear = () => { + setTemplate("none"); + handleMapPoolChange(MapPool.EMPTY); + }; - const handleTemplateChange = (template: MapPoolTemplateValue) => { - setTemplate(template); + const handleTemplateChange = (template: MapPoolTemplateValue) => { + setTemplate(template); - if (template === "none") { - return; - } + if (template === "none") { + return; + } - if (template === "event") { - // If the user selected the "event" option, the _initial_ event passed via - // props is likely not the current state and should not be prefilled - // anymore. - setInitialSerializedEvent(undefined); - return; - } + if (template === "event") { + // If the user selected the "event" option, the _initial_ event passed via + // props is likely not the current state and should not be prefilled + // anymore. + setInitialSerializedEvent(undefined); + return; + } - if (startsWith(template, "preset:")) { - const [, presetId] = split(template, ":"); + if (startsWith(template, "preset:")) { + const [, presetId] = split(template, ":"); - handleMapPoolChange(MapPool[presetId]); - return; - } + handleMapPoolChange(MapPool[presetId]); + return; + } - if (startsWith(template, "recent-event:")) { - const [, eventId] = split(template, ":"); + if (startsWith(template, "recent-event:")) { + const [, eventId] = split(template, ":"); - const event = recentEvents?.find((e) => e.id.toString() === eventId); + const event = recentEvents?.find((e) => e.id.toString() === eventId); - if (event) { - handleMapPoolChange(new MapPool(event.serializedMapPool), event); - } - return; - } + if (event) { + handleMapPoolChange(new MapPool(event.serializedMapPool), event); + } + return; + } - assertType(); - }; + assertType(); + }; - return ( -
    - {Boolean(title) && {title}} - {Boolean(handleRemoval || allowBulkEdit) && ( -
    - {handleRemoval && ( - - )} - {allowBulkEdit && ( - - )} -
    - )} -
    - {allowBulkEdit && ( -
    - - {template === "event" && ( - - )} -
    - )} - {info} - - {footer} -
    -
    - ); + return ( +
    + {Boolean(title) && {title}} + {Boolean(handleRemoval || allowBulkEdit) && ( +
    + {handleRemoval && ( + + )} + {allowBulkEdit && ( + + )} +
    + )} +
    + {allowBulkEdit && ( +
    + + {template === "event" && ( + + )} +
    + )} + {info} + + {footer} +
    +
    + ); } export type MapPoolStagesProps = { - mapPool: MapPool; - handleMapPoolChange?: (newMapPool: MapPool) => void; - allowBulkEdit?: boolean; - modesToInclude?: ModeShort[]; - preselectedMapPool?: MapPool; - hideBanned?: boolean; + mapPool: MapPool; + handleMapPoolChange?: (newMapPool: MapPool) => void; + allowBulkEdit?: boolean; + modesToInclude?: ModeShort[]; + preselectedMapPool?: MapPool; + hideBanned?: boolean; }; export function MapPoolStages({ - mapPool, - handleMapPoolChange, - allowBulkEdit = false, - modesToInclude, - preselectedMapPool, - hideBanned = false, + mapPool, + handleMapPoolChange, + allowBulkEdit = false, + modesToInclude, + preselectedMapPool, + hideBanned = false, }: MapPoolStagesProps) { - const { t } = useTranslation(["game-misc", "common"]); + const { t } = useTranslation(["game-misc", "common"]); - const isPresentational = !handleMapPoolChange; + const isPresentational = !handleMapPoolChange; - const stageRowIsVisible = (stageId: StageId) => { - if (!isPresentational) return true; + const stageRowIsVisible = (stageId: StageId) => { + if (!isPresentational) return true; - return mapPool.hasStage(stageId); - }; + return mapPool.hasStage(stageId); + }; - const handleModeChange = ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => { - const newMapPool = mapPool.parsed[mode].includes(stageId) - ? new MapPool({ - ...mapPool.parsed, - [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), - }) - : new MapPool({ - ...mapPool.parsed, - [mode]: [...mapPool.parsed[mode], stageId], - }); + const handleModeChange = ({ + mode, + stageId, + }: { + mode: ModeShort; + stageId: StageId; + }) => { + const newMapPool = mapPool.parsed[mode].includes(stageId) + ? new MapPool({ + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + }) + : new MapPool({ + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + }); - handleMapPoolChange?.(newMapPool); - }; + handleMapPoolChange?.(newMapPool); + }; - const handleStageClear = (stageId: StageId) => { - const newMapPool = new MapPool({ - TW: mapPool.parsed.TW.filter((id) => id !== stageId), - SZ: mapPool.parsed.SZ.filter((id) => id !== stageId), - TC: mapPool.parsed.TC.filter((id) => id !== stageId), - RM: mapPool.parsed.RM.filter((id) => id !== stageId), - CB: mapPool.parsed.CB.filter((id) => id !== stageId), - }); + const handleStageClear = (stageId: StageId) => { + const newMapPool = new MapPool({ + TW: mapPool.parsed.TW.filter((id) => id !== stageId), + SZ: mapPool.parsed.SZ.filter((id) => id !== stageId), + TC: mapPool.parsed.TC.filter((id) => id !== stageId), + RM: mapPool.parsed.RM.filter((id) => id !== stageId), + CB: mapPool.parsed.CB.filter((id) => id !== stageId), + }); - handleMapPoolChange?.(newMapPool); - }; + handleMapPoolChange?.(newMapPool); + }; - const handleStageAdd = (stageId: StageId) => { - const newMapPool = new MapPool({ - TW: [...mapPool.parsed.TW, stageId], - SZ: [...mapPool.parsed.SZ, stageId], - TC: [...mapPool.parsed.TC, stageId], - RM: [...mapPool.parsed.RM, stageId], - CB: [...mapPool.parsed.CB, stageId], - }); + const handleStageAdd = (stageId: StageId) => { + const newMapPool = new MapPool({ + TW: [...mapPool.parsed.TW, stageId], + SZ: [...mapPool.parsed.SZ, stageId], + TC: [...mapPool.parsed.TC, stageId], + RM: [...mapPool.parsed.RM, stageId], + CB: [...mapPool.parsed.CB, stageId], + }); - handleMapPoolChange?.(newMapPool); - }; + handleMapPoolChange?.(newMapPool); + }; - const id = React.useId(); + const id = React.useId(); - return ( -
    - {stageIds.filter(stageRowIsVisible).map((stageId) => ( -
    - -
    -
    - {t(`game-misc:STAGE_${stageId}`)} -
    -
    - {modes - .filter( - (mode) => - !modesToInclude || modesToInclude.includes(mode.short), - ) - .map((mode) => { - const selected = mapPool.has({ stageId, mode: mode.short }); + return ( +
    + {stageIds.filter(stageRowIsVisible).map((stageId) => ( +
    + +
    +
    + {t(`game-misc:STAGE_${stageId}`)} +
    +
    + {modes + .filter( + (mode) => + !modesToInclude || modesToInclude.includes(mode.short), + ) + .map((mode) => { + const selected = mapPool.has({ stageId, mode: mode.short }); - if (isPresentational && !selected) return null; - if (isPresentational && selected) { - return ( - {t(`game-misc:MODE_LONG_${mode.short}`)} - ); - } + if (isPresentational && !selected) return null; + if (isPresentational && selected) { + return ( + {t(`game-misc:MODE_LONG_${mode.short}`)} + ); + } - const preselected = preselectedMapPool?.has({ - stageId, - mode: mode.short, - }); + const preselected = preselectedMapPool?.has({ + stageId, + mode: mode.short, + }); - return ( - - ); - })} - {!isPresentational && - allowBulkEdit && - (mapPool.hasStage(stageId) ? ( -
    -
    -
    - ))} -
    - ); + return ( + + ); + })} + {!isPresentational && + allowBulkEdit && + (mapPool.hasStage(stageId) ? ( +
    +
    +
    + ))} +
    + ); } type MapModePresetId = "ANARCHY" | "ALL" | ModeShort; @@ -358,102 +358,102 @@ type MapModePresetId = "ANARCHY" | "ALL" | ModeShort; const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort]; type MapPoolTemplateValue = - | "none" - | `preset:${MapModePresetId}` - | `recent-event:${string}` - | "event"; + | "none" + | `preset:${MapModePresetId}` + | `recent-event:${string}` + | "event"; function detectTemplate(mapPool: MapPool): MapPoolTemplateValue { - for (const presetId of presetIds) { - if (MapPool[presetId].serialized === mapPool.serialized) { - return `preset:${presetId}`; - } - } - return "none"; + for (const presetId of presetIds) { + if (MapPool[presetId].serialized === mapPool.serialized) { + return `preset:${presetId}`; + } + } + return "none"; } type MapPoolTemplateSelectProps = { - value: MapPoolTemplateValue; - handleChange: (newValue: MapPoolTemplateValue) => void; - recentEvents?: Pick[]; + value: MapPoolTemplateValue; + handleChange: (newValue: MapPoolTemplateValue) => void; + recentEvents?: Pick[]; }; function MapPoolTemplateSelect({ - handleChange, - value, - recentEvents, + handleChange, + value, + recentEvents, }: MapPoolTemplateSelectProps) { - const { t } = useTranslation(["game-misc", "common"]); + const { t } = useTranslation(["game-misc", "common"]); - return ( - - ); + return ( + + ); } type TemplateEventSelectionProps = { - handleEventChange: ( - mapPool: MapPool, - event?: Pick, - ) => void; - initialEvent?: SerializedMapPoolEvent; + handleEventChange: ( + mapPool: MapPool, + event?: Pick, + ) => void; + initialEvent?: SerializedMapPoolEvent; }; function TemplateEventSelection({ - handleEventChange, - initialEvent, + handleEventChange, + initialEvent, }: TemplateEventSelectionProps) { - const { t } = useTranslation(); - const id = React.useId(); + const { t } = useTranslation(); + const id = React.useId(); - return ( - - ); + return ( + + ); } diff --git a/app/components/Menu.tsx b/app/components/Menu.tsx index 606157166..198c03b50 100644 --- a/app/components/Menu.tsx +++ b/app/components/Menu.tsx @@ -1,59 +1,60 @@ import { Menu as HeadlessUIMenu, Transition } from "@headlessui/react"; -import * as React from "react"; import clsx from "clsx"; +import * as React from "react"; export function Menu({ - button, - items, - className, + button, + items, + className, }: { - button: React.ElementType; - items: { - // type: "button"; TODO: type: "link" - text: string; - id: string; - icon?: React.ReactNode; - onClick: () => void; - disabled?: boolean; - }[]; - className?: string; + button: React.ElementType; + items: { + // type: "button"; TODO: type: "link" + text: string; + id: string; + icon?: React.ReactNode; + onClick: () => void; + disabled?: boolean; + }[]; + className?: string; }) { - return ( - - - - - {items.map((item) => { - return ( - - {({ active }) => ( - - )} - - ); - })} - - - - ); + return ( + + + + + {items.map((item) => { + return ( + + {({ active }) => ( + + )} + + ); + })} + + + + ); } diff --git a/app/components/NewTabs.tsx b/app/components/NewTabs.tsx index 43cbb7f04..16224ab38 100644 --- a/app/components/NewTabs.tsx +++ b/app/components/NewTabs.tsx @@ -3,135 +3,135 @@ import clsx from "clsx"; import * as React from "react"; interface NewTabsProps { - tabs: { - label: string; - number?: number; - hidden?: boolean; - }[]; - content: { - key: string; - element: React.ReactNode; - hidden?: boolean; - unmount?: boolean; - }[]; - scrolling?: boolean; - selectedIndex?: number; - setSelectedIndex?: (index: number) => void; - /** Don't take space when no tabs to show? */ - disappearing?: boolean; - type?: "divider"; - sticky?: boolean; + tabs: { + label: string; + number?: number; + hidden?: boolean; + }[]; + content: { + key: string; + element: React.ReactNode; + hidden?: boolean; + unmount?: boolean; + }[]; + scrolling?: boolean; + selectedIndex?: number; + setSelectedIndex?: (index: number) => void; + /** Don't take space when no tabs to show? */ + disappearing?: boolean; + type?: "divider"; + sticky?: boolean; } export function NewTabs(args: NewTabsProps) { - if (args.type === "divider") { - return ; - } + if (args.type === "divider") { + return ; + } - const { - tabs, - content, - scrolling = true, - selectedIndex, - setSelectedIndex, - disappearing = false, - } = args; + const { + tabs, + content, + scrolling = true, + selectedIndex, + setSelectedIndex, + disappearing = false, + } = args; - const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1; + const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1; - return ( - - - {tabs - .filter((t) => !t.hidden) - .map((tab) => { - return ( - - {tab.label} - {typeof tab.number === "number" && tab.number !== 0 && ( - {tab.number} - )} - - ); - })} - - - {content - .filter((c) => !c.hidden) - .map((c) => { - return ( - - {c.element} - - ); - })} - - - ); + return ( + + + {tabs + .filter((t) => !t.hidden) + .map((tab) => { + return ( + + {tab.label} + {typeof tab.number === "number" && tab.number !== 0 && ( + {tab.number} + )} + + ); + })} + + + {content + .filter((c) => !c.hidden) + .map((c) => { + return ( + + {c.element} + + ); + })} + + + ); } function DividerTabs({ - tabs, - content, - scrolling = true, - selectedIndex, - setSelectedIndex, - disappearing = false, + tabs, + content, + scrolling = true, + selectedIndex, + setSelectedIndex, + disappearing = false, }: NewTabsProps) { - const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1; + const cantSwitchTabs = tabs.filter((t) => !t.hidden).length <= 1; - return ( - - - {tabs - .filter((t) => !t.hidden) - .map((tab, i) => { - return ( - - - {tab.label} - {typeof tab.number === "number" && tab.number !== 0 && ( - ({tab.number}) - )} - - {i !== tabs.length - 1 && ( -
    - )} - - ); - })} - - - {content - .filter((c) => !c.hidden) - .map((c) => { - return {c.element}; - })} - - - ); + return ( + + + {tabs + .filter((t) => !t.hidden) + .map((tab, i) => { + return ( + + + {tab.label} + {typeof tab.number === "number" && tab.number !== 0 && ( + ({tab.number}) + )} + + {i !== tabs.length - 1 && ( +
    + )} + + ); + })} + + + {content + .filter((c) => !c.hidden) + .map((c) => { + return {c.element}; + })} + + + ); } diff --git a/app/components/Pagination.tsx b/app/components/Pagination.tsx index 3d6bc2d98..13cef994a 100644 --- a/app/components/Pagination.tsx +++ b/app/components/Pagination.tsx @@ -5,48 +5,48 @@ import { ArrowRightIcon } from "~/components/icons/ArrowRight"; import { nullFilledArray } from "~/utils/arrays"; export function Pagination({ - currentPage, - pagesCount, - nextPage, - previousPage, - setPage, + currentPage, + pagesCount, + nextPage, + previousPage, + setPage, }: { - currentPage: number; - pagesCount: number; - nextPage: () => void; - previousPage: () => void; - setPage: (page: number) => void; + currentPage: number; + pagesCount: number; + nextPage: () => void; + previousPage: () => void; + setPage: (page: number) => void; }) { - return ( -
    -
    - ); + return ( +
    +
    + ); } diff --git a/app/components/Placement.tsx b/app/components/Placement.tsx index a494f215c..a556caa0a 100644 --- a/app/components/Placement.tsx +++ b/app/components/Placement.tsx @@ -1,75 +1,75 @@ import { useTranslation } from "react-i18next"; import { - FIRST_PLACEMENT_ICON_PATH, - SECOND_PLACEMENT_ICON_PATH, - THIRD_PLACEMENT_ICON_PATH, + FIRST_PLACEMENT_ICON_PATH, + SECOND_PLACEMENT_ICON_PATH, + THIRD_PLACEMENT_ICON_PATH, } from "~/utils/urls"; export type PlacementProps = { - placement: number; - iconClassName?: string; - textClassName?: string; - size?: number; - textOnly?: boolean; - showAsSuperscript?: boolean; + placement: number; + iconClassName?: string; + textClassName?: string; + size?: number; + textOnly?: boolean; + showAsSuperscript?: boolean; }; const getSpecialPlacementIconPath = (placement: number): string | null => { - switch (placement) { - case 3: - return THIRD_PLACEMENT_ICON_PATH; - case 2: - return SECOND_PLACEMENT_ICON_PATH; - case 1: - return FIRST_PLACEMENT_ICON_PATH; - default: - return null; - } + switch (placement) { + case 3: + return THIRD_PLACEMENT_ICON_PATH; + case 2: + return SECOND_PLACEMENT_ICON_PATH; + case 1: + return FIRST_PLACEMENT_ICON_PATH; + default: + return null; + } }; export function Placement({ - placement, - iconClassName, - textClassName, - size = 20, - textOnly = false, - showAsSuperscript = true, + placement, + iconClassName, + textClassName, + size = 20, + textOnly = false, + showAsSuperscript = true, }: PlacementProps) { - const { t } = useTranslation(undefined, {}); + const { t } = useTranslation(undefined, {}); - // Remove assertion if types stop claiming result is "never". - const ordinalSuffix: string = t("results.placeSuffix", { - count: placement, - ordinal: true, - // no suffix is a better default than english - defaultValue: "", - fallbackLng: [], - }); + // Remove assertion if types stop claiming result is "never". + const ordinalSuffix: string = t("results.placeSuffix", { + count: placement, + ordinal: true, + // no suffix is a better default than english + defaultValue: "", + fallbackLng: [], + }); - const isSuperscript = showAsSuperscript && ordinalSuffix.startsWith("^"); - const ordinalSuffixText = ordinalSuffix.replace(/^\^/, ""); + const isSuperscript = showAsSuperscript && ordinalSuffix.startsWith("^"); + const ordinalSuffixText = ordinalSuffix.replace(/^\^/, ""); - const iconPath = textOnly ? null : getSpecialPlacementIconPath(placement); + const iconPath = textOnly ? null : getSpecialPlacementIconPath(placement); - if (!iconPath) { - return ( - - {placement} - {isSuperscript ? {ordinalSuffixText} : ordinalSuffixText} - - ); - } + if (!iconPath) { + return ( + + {placement} + {isSuperscript ? {ordinalSuffixText} : ordinalSuffixText} + + ); + } - const placementString = `${placement}${ordinalSuffixText}`; + const placementString = `${placement}${ordinalSuffixText}`; - return ( - {placementString} - ); + return ( + {placementString} + ); } diff --git a/app/components/Popover.tsx b/app/components/Popover.tsx index 691d13582..bc682f61f 100644 --- a/app/components/Popover.tsx +++ b/app/components/Popover.tsx @@ -8,62 +8,62 @@ import { useIsMounted } from "~/hooks/useIsMounted"; // TODO: after clicking item in the pop over panel should close it export function Popover({ - children, - buttonChildren, - triggerClassName, - triggerTestId, - containerClassName, - contentClassName, - placement, + children, + buttonChildren, + triggerClassName, + triggerTestId, + containerClassName, + contentClassName, + placement, }: { - children: React.ReactNode; - buttonChildren: React.ReactNode; - triggerClassName?: string; - triggerTestId?: string; - containerClassName?: string; - contentClassName?: string; - placement?: Placement; + children: React.ReactNode; + buttonChildren: React.ReactNode; + triggerClassName?: string; + triggerTestId?: string; + containerClassName?: string; + contentClassName?: string; + placement?: Placement; }) { - const [referenceElement, setReferenceElement] = React.useState(); - const isMounted = useIsMounted(); - const [popperElement, setPopperElement] = React.useState(); - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement, - modifiers: [ - { - name: "offset", - options: { - offset: [0, 8], - }, - }, - ], - }); + const [referenceElement, setReferenceElement] = React.useState(); + const isMounted = useIsMounted(); + const [popperElement, setPopperElement] = React.useState(); + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + modifiers: [ + { + name: "offset", + options: { + offset: [0, 8], + }, + }, + ], + }); - return ( - - - {buttonChildren} - + return ( + + + {buttonChildren} + - {isMounted - ? createPortal( - - {children} - , - document.body, - ) - : null} - - ); + {isMounted + ? createPortal( + + {children} + , + document.body, + ) + : null} + + ); } diff --git a/app/components/Redirect.tsx b/app/components/Redirect.tsx index bfb38190c..873300dfc 100644 --- a/app/components/Redirect.tsx +++ b/app/components/Redirect.tsx @@ -2,11 +2,11 @@ import { useNavigate } from "@remix-run/react"; import * as React from "react"; export function Redirect({ to }: { to: string }) { - const navigate = useNavigate(); + const navigate = useNavigate(); - React.useEffect(() => { - navigate(to); - }, [navigate, to]); + React.useEffect(() => { + navigate(to); + }, [navigate, to]); - return null; + return null; } diff --git a/app/components/RelativeTime.tsx b/app/components/RelativeTime.tsx index d8cd4d45e..7c9fa3e75 100644 --- a/app/components/RelativeTime.tsx +++ b/app/components/RelativeTime.tsx @@ -2,29 +2,29 @@ import type * as React from "react"; import { useIsMounted } from "~/hooks/useIsMounted"; export function RelativeTime({ - children, - timestamp, + children, + timestamp, }: { - children: React.ReactNode; - timestamp: number; + children: React.ReactNode; + timestamp: number; }) { - const isMounted = useIsMounted(); + const isMounted = useIsMounted(); - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/app/components/RequiredHiddenInput.tsx b/app/components/RequiredHiddenInput.tsx index 9435917d8..259535c82 100644 --- a/app/components/RequiredHiddenInput.tsx +++ b/app/components/RequiredHiddenInput.tsx @@ -1,21 +1,21 @@ export function RequiredHiddenInput({ - value, - isValid, - name, + value, + isValid, + name, }: { - value: string; - isValid: boolean; - name: string; + value: string; + isValid: boolean; + name: string; }) { - return ( - null} - required - /> - ); + return ( + null} + required + /> + ); } diff --git a/app/components/Section.tsx b/app/components/Section.tsx index fbe6b3440..2e4e6bfd3 100644 --- a/app/components/Section.tsx +++ b/app/components/Section.tsx @@ -1,16 +1,16 @@ export function Section({ - title, - children, - className, + title, + children, + className, }: { - title?: string; - children: React.ReactNode; - className?: string; + title?: string; + children: React.ReactNode; + className?: string; }) { - return ( -
    - {title &&

    {title}

    } -
    {children}
    -
    - ); + return ( +
    + {title &&

    {title}

    } +
    {children}
    +
    + ); } diff --git a/app/components/SubNav.tsx b/app/components/SubNav.tsx index 78e3bd3f6..55a87bdc9 100644 --- a/app/components/SubNav.tsx +++ b/app/components/SubNav.tsx @@ -4,63 +4,63 @@ import clsx from "clsx"; import type * as React from "react"; export function SubNav({ - children, - secondary, + children, + secondary, }: { - children: React.ReactNode; - secondary?: boolean; + children: React.ReactNode; + secondary?: boolean; }) { - return ( -
    - -
    - ); + return ( +
    + +
    + ); } export function SubNavLink({ - children, - className, - end = true, - secondary = false, - controlled = false, - active = false, - ...props + children, + className, + end = true, + secondary = false, + controlled = false, + active = false, + ...props }: LinkProps & { - end?: boolean; - children: React.ReactNode; - secondary?: boolean; - controlled?: boolean; - active?: boolean; + end?: boolean; + children: React.ReactNode; + secondary?: boolean; + controlled?: boolean; + active?: boolean; }) { - return ( - - clsx("sub-nav__link__container", { - active: controlled ? active : state.isActive, - pending: state.isPending, - }) - } - end={end} - {...props} - > -
    - {children} -
    -
    - - ); + return ( + + clsx("sub-nav__link__container", { + active: controlled ? active : state.isActive, + pending: state.isPending, + }) + } + end={end} + {...props} + > +
    + {children} +
    +
    + + ); } diff --git a/app/components/SubmitButton.tsx b/app/components/SubmitButton.tsx index 106f56582..c7d65e9d5 100644 --- a/app/components/SubmitButton.tsx +++ b/app/components/SubmitButton.tsx @@ -2,46 +2,46 @@ import { type FetcherWithComponents, useNavigation } from "@remix-run/react"; import { Button, type ButtonProps } from "./Button"; interface SubmitButtonProps extends ButtonProps { - /** If the page has multiple forms you can pass in fetcher.state to differentiate when this SubmitButton should be in submitting state */ - state?: FetcherWithComponents["state"]; - _action?: string; + /** If the page has multiple forms you can pass in fetcher.state to differentiate when this SubmitButton should be in submitting state */ + state?: FetcherWithComponents["state"]; + _action?: string; } export function SubmitButton({ - children, - state, - _action, - testId, - ...rest + children, + state, + _action, + testId, + ...rest }: SubmitButtonProps) { - const navigation = useNavigation(); + const navigation = useNavigation(); - const isSubmitting = state ? state !== "idle" : navigation.state !== "idle"; + const isSubmitting = state ? state !== "idle" : navigation.state !== "idle"; - const name = () => { - if (rest.name) return rest.name; - if (_action) return "_action"; + const name = () => { + if (rest.name) return rest.name; + if (_action) return "_action"; - return undefined; - }; + return undefined; + }; - const value = () => { - if (rest.value) return rest.value; - if (_action) return _action; + const value = () => { + if (rest.value) return rest.value; + if (_action) return _action; - return undefined; - }; + return undefined; + }; - return ( - - ); + return ( + + ); } diff --git a/app/components/Table.tsx b/app/components/Table.tsx index 756b392bd..5bcd5a787 100644 --- a/app/components/Table.tsx +++ b/app/components/Table.tsx @@ -1,3 +1,3 @@ export function Table({ children }: { children: React.ReactNode }) { - return {children}
    ; + return {children}
    ; } diff --git a/app/components/Tabs.tsx b/app/components/Tabs.tsx index 64af88108..e3ab03ff2 100644 --- a/app/components/Tabs.tsx +++ b/app/components/Tabs.tsx @@ -4,46 +4,46 @@ import type * as React from "react"; // shares styles with SubNav.tsx export function Tabs({ - children, - className, - compact = false, + children, + className, + compact = false, }: { - children: React.ReactNode; - className?: string; - compact?: boolean; + children: React.ReactNode; + className?: string; + compact?: boolean; }) { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); } export function Tab({ - children, - className, - active, - onClick, - testId, + children, + className, + active, + onClick, + testId, }: { - children: React.ReactNode; - className?: string; - active: boolean; - onClick: () => void; - testId?: string; + children: React.ReactNode; + className?: string; + active: boolean; + onClick: () => void; + testId?: string; }) { - // TODO: improve semantic html here, maybe could use tab component from Headless UI? - return ( -
    -
    {children}
    -
    -
    - ); + // TODO: improve semantic html here, maybe could use tab component from Headless UI? + return ( +
    +
    {children}
    +
    +
    + ); } diff --git a/app/components/Toggle.tsx b/app/components/Toggle.tsx index b91d5ad8b..24aee4690 100644 --- a/app/components/Toggle.tsx +++ b/app/components/Toggle.tsx @@ -2,31 +2,31 @@ import { Switch } from "@headlessui/react"; import clsx from "clsx"; export function Toggle({ - checked, - setChecked, - tiny, - id, - name, - disabled, + checked, + setChecked, + tiny, + id, + name, + disabled, }: { - checked: boolean; - setChecked: (checked: boolean) => void; - tiny?: boolean; - id?: string; - name?: string; - disabled?: boolean; + checked: boolean; + setChecked: (checked: boolean) => void; + tiny?: boolean; + id?: string; + name?: string; + disabled?: boolean; }) { - return ( - - - - ); + return ( + + + + ); } diff --git a/app/components/UserSearch.tsx b/app/components/UserSearch.tsx index 1ce6fc1e9..4c6371dbc 100644 --- a/app/components/UserSearch.tsx +++ b/app/components/UserSearch.tsx @@ -2,137 +2,137 @@ import { Combobox } from "@headlessui/react"; import { useFetcher } from "@remix-run/react"; import clsx from "clsx"; import * as React from "react"; +import { useTranslation } from "react-i18next"; import { useDebounce } from "react-use"; import type { UserSearchLoaderData } from "~/features/user-search/routes/u"; import { Avatar } from "./Avatar"; -import { useTranslation } from "react-i18next"; type UserSearchUserItem = NonNullable["users"][number]; export function UserSearch({ - inputName, - onChange, - initialUserId, - id, - className, - userIdsToOmit, - required, + inputName, + onChange, + initialUserId, + id, + className, + userIdsToOmit, + required, }: { - inputName: string; - onChange?: (user: UserSearchUserItem) => void; - initialUserId?: number; - id?: string; - className?: string; - userIdsToOmit?: Set; - required?: boolean; + inputName: string; + onChange?: (user: UserSearchUserItem) => void; + initialUserId?: number; + id?: string; + className?: string; + userIdsToOmit?: Set; + required?: boolean; }) { - const { t } = useTranslation(); - const [selectedUser, setSelectedUser] = - React.useState(null); - const queryFetcher = useFetcher(); - const initialUserFetcher = useFetcher(); - const [query, setQuery] = React.useState(""); - useDebounce( - () => { - if (!query) return; + const { t } = useTranslation(); + const [selectedUser, setSelectedUser] = + React.useState(null); + const queryFetcher = useFetcher(); + const initialUserFetcher = useFetcher(); + const [query, setQuery] = React.useState(""); + useDebounce( + () => { + if (!query) return; - queryFetcher.load(`/u?q=${query}&limit=6`); - }, - 1000, - [query], - ); + queryFetcher.load(`/u?q=${query}&limit=6`); + }, + 1000, + [query], + ); - // load initial user - React.useEffect(() => { - if ( - !initialUserId || - initialUserFetcher.state !== "idle" || - initialUserFetcher.data - ) { - return; - } + // load initial user + React.useEffect(() => { + if ( + !initialUserId || + initialUserFetcher.state !== "idle" || + initialUserFetcher.data + ) { + return; + } - initialUserFetcher.load(`/u?q=${initialUserId}`); - }, [initialUserId, initialUserFetcher]); - React.useEffect(() => { - if (!initialUserFetcher.data) return; + initialUserFetcher.load(`/u?q=${initialUserId}`); + }, [initialUserId, initialUserFetcher]); + React.useEffect(() => { + if (!initialUserFetcher.data) return; - setSelectedUser(initialUserFetcher.data.users[0]); - }, [initialUserFetcher.data]); + setSelectedUser(initialUserFetcher.data.users[0]); + }, [initialUserFetcher.data]); - const allUsers = queryFetcher.data?.users ?? []; + const allUsers = queryFetcher.data?.users ?? []; - const users = allUsers.filter((u) => !userIdsToOmit?.has(u.id)); - const noMatches = queryFetcher.data && users.length === 0; + const users = allUsers.filter((u) => !userIdsToOmit?.has(u.id)); + const noMatches = queryFetcher.data && users.length === 0; - const initialSelectionIsLoading = Boolean( - initialUserId && !initialUserFetcher.data, - ); + const initialSelectionIsLoading = Boolean( + initialUserId && !initialUserFetcher.data, + ); - return ( -
    - {selectedUser && inputName ? ( - - ) : null} - { - setSelectedUser(newUser); - onChange?.(newUser!); - }} - disabled={initialSelectionIsLoading} - > - setQuery(event.target.value)} - displayValue={(user: UserSearchUserItem) => user?.username ?? ""} - className={clsx("combobox-input", className)} - data-1p-ignore - data-testid={`${inputName}-combobox-input`} - id={id} - required={required} - /> - - {noMatches ? ( -
    - {t("forms.errors.noSearchMatches")}{" "} - 🤔 -
    - ) : null} - {users.map((user, i) => ( - - {({ active }) => ( -
  • - -
    -
    - {user.username}{" "} - {user.plusTier ? ( - +{user.plusTier} - ) : null} -
    - {user.discordUniqueName ? ( -
    {user.discordUniqueName}
    - ) : null} -
    -
  • - )} -
    - ))} -
    -
    -
    - ); + return ( +
    + {selectedUser && inputName ? ( + + ) : null} + { + setSelectedUser(newUser); + onChange?.(newUser!); + }} + disabled={initialSelectionIsLoading} + > + setQuery(event.target.value)} + displayValue={(user: UserSearchUserItem) => user?.username ?? ""} + className={clsx("combobox-input", className)} + data-1p-ignore + data-testid={`${inputName}-combobox-input`} + id={id} + required={required} + /> + + {noMatches ? ( +
    + {t("forms.errors.noSearchMatches")}{" "} + 🤔 +
    + ) : null} + {users.map((user, i) => ( + + {({ active }) => ( +
  • + +
    +
    + {user.username}{" "} + {user.plusTier ? ( + +{user.plusTier} + ) : null} +
    + {user.discordUniqueName ? ( +
    {user.discordUniqueName}
    + ) : null} +
    +
  • + )} +
    + ))} +
    +
    +
    + ); } diff --git a/app/components/YouTubeEmbed.tsx b/app/components/YouTubeEmbed.tsx index 3399327be..d6bf02b33 100644 --- a/app/components/YouTubeEmbed.tsx +++ b/app/components/YouTubeEmbed.tsx @@ -1,24 +1,24 @@ export function YouTubeEmbed({ - id, - start, - autoplay = false, + id, + start, + autoplay = false, }: { - id: string; - start?: number; - autoplay?: boolean; + id: string; + start?: number; + autoplay?: boolean; }) { - return ( -
    -