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 ( -
    -