BIome v2 upgrade
Some checks are pending
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2025-06-22 16:49:27 +03:00
parent 02f705b45f
commit dd1adad94b
314 changed files with 1093 additions and 1025 deletions

View File

@@ -4,11 +4,10 @@ import { abilities } from "~/modules/in-game-lists/abilities";
import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists/types";
import invariant from "~/utils/invariant";
import { abilityImageUrl } from "~/utils/urls";
import styles from "./AbilitiesSelector.module.css";
import { Ability } from "./Ability";
import { Image } from "./Image";
import styles from "./AbilitiesSelector.module.css";
interface AbilitiesSelectorProps {
selectedAbilities: BuildAbilitiesTupleWithUnknown;
onChange: (newAbilities: BuildAbilitiesTupleWithUnknown) => void;

View File

@@ -3,9 +3,8 @@ 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 styles from "./Ability.module.css";
import { Image } from "./Image";
const sizeMap = {
MAIN: 42,

View File

@@ -1,5 +1,5 @@
import { Image } from "~/components/Image";
import { LinkButton } from "~/components/elements/Button";
import { Image } from "~/components/Image";
import { PlusIcon } from "~/components/icons/Plus";
import { navIconUrl } from "~/utils/urls";

View File

@@ -47,7 +47,6 @@ export function Avatar({
: BLANK_IMAGE_URL); // avoid broken image placeholder
return (
// biome-ignore lint/a11y/useAltText: spread messes it up https://github.com/biomejs/biome/issues/3081
<img
className={clsx("avatar", className)}
src={src}

View File

@@ -19,7 +19,6 @@ export function Badge({ badge, onClick, isAnimated, size }: BadgeProps) {
if (isAnimated) {
return (
// biome-ignore lint/a11y/useAltText: false positive..?
<img
src={badgeUrl({ code: badge.code, extension: "gif" })}
alt={badge.displayName}

View File

@@ -7,9 +7,9 @@ import type { BuildWeaponWithTop500Info } from "~/features/builds/queries/builds
import { useIsMounted } from "~/hooks/useIsMounted";
import type {
Ability as AbilityType,
BuildAbilitiesTuple,
ModeShort,
} from "~/modules/in-game-lists/types";
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";
@@ -24,17 +24,16 @@ import {
weaponBuildPage,
} from "~/utils/urls";
import { Ability } from "./Ability";
import { FormWithConfirm } from "./FormWithConfirm";
import { Image } from "./Image";
import styles from "./BuildCard.module.css";
import { LinkButton, SendouButton } from "./elements/Button";
import { SendouPopover } from "./elements/Popover";
import { FormWithConfirm } from "./FormWithConfirm";
import { Image } from "./Image";
import { EditIcon } from "./icons/Edit";
import { LockIcon } from "./icons/Lock";
import { SpeechBubbleIcon } from "./icons/SpeechBubble";
import { TrashIcon } from "./icons/Trash";
import styles from "./BuildCard.module.css";
interface BuildProps {
build: Pick<
Tables["Build"],

View File

@@ -11,9 +11,9 @@ import {
LOG_IN_URL,
SENDOU_INK_DISCORD_URL,
} from "~/utils/urls";
import { SendouButton } from "./elements/Button";
import { Image } from "./Image";
import { Main } from "./Main";
import { SendouButton } from "./elements/Button";
export function Catcher() {
const error = useRouteError();
@@ -53,14 +53,12 @@ export function Catcher() {
<a href={SENDOU_INK_DISCORD_URL}>our Discord</a> so it can be fixed.
</p>
{errorText ? (
<>
<div className="mt-4 stack sm items-center">
<textarea readOnly defaultValue={errorText} />
<SendouButton onPress={() => copyToClipboard(errorText)}>
Copy to clipboard
</SendouButton>
</div>
</>
<div className="mt-4 stack sm items-center">
<textarea readOnly defaultValue={errorText} />
<SendouButton onPress={() => copyToClipboard(errorText)}>
Copy to clipboard
</SendouButton>
</div>
) : null}
</Main>
);

View File

@@ -3,11 +3,11 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useDebounce } from "react-use";
import { CUSTOM_CSS_VAR_COLORS } from "~/features/user-page/user-page-constants";
import { InfoPopover } from "./InfoPopover";
import { Label } from "./Label";
import { SendouButton } from "./elements/Button";
import { InfoPopover } from "./InfoPopover";
import { AlertIcon } from "./icons/Alert";
import { CheckmarkIcon } from "./icons/Checkmark";
import { Label } from "./Label";
type CustomColorsRecord = Partial<
Record<(typeof CUSTOM_CSS_VAR_COLORS)[number], string>

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import { useIsMounted } from "~/hooks/useIsMounted";
import { dateToYearMonthDayHourMinuteString, isValidDate } from "~/utils/dates";
import { logger } from "~/utils/logger";
export interface DateInputProps
extends Omit<
@@ -33,7 +34,7 @@ export function DateInput({
if (isValidDate(defaultValue)) {
return [defaultValue, dateToYearMonthDayHourMinuteString(defaultValue)];
}
console.warn("DateInput got invalid date as defaultValue");
logger.warn("DateInput got invalid date as defaultValue");
}
return [null, ""];
});

View File

@@ -2,11 +2,7 @@ import { useActionData } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import type { Namespace } from "~/modules/i18n/resources.server";
export function FormErrors({
namespace,
}: {
namespace: Namespace;
}) {
export function FormErrors({ namespace }: { namespace: Namespace }) {
const { t } = useTranslation(["common", namespace]);
const actionData = useActionData<{ errors?: string[] }>();

View File

@@ -1,5 +1,6 @@
import { useFetcher } from "@remix-run/react";
import clsx from "clsx";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Input } from "~/components/Input";
import { Label } from "~/components/Label";
@@ -14,6 +15,7 @@ export function FriendCodeInput({
}) {
const fetcher = useFetcher();
const { t } = useTranslation(["common"]);
const id = React.useId();
return (
<fetcher.Form method="post" action={SENDOUQ_PAGE}>
@@ -24,14 +26,14 @@ export function FriendCodeInput({
>
<div>
{!friendCode ? (
<Label htmlFor="friendCode">{t("common:fc.title")}</Label>
<Label htmlFor={id}>{t("common:fc.title")}</Label>
) : null}
{friendCode ? (
<div className="font-bold">SW-{friendCode}</div>
) : (
<Input
leftAddon="SW-"
id="friendCode"
id={id}
name="friendCode"
pattern={FRIEND_CODE_REGEXP_PATTERN}
placeholder="1234-5678-9012"

View File

@@ -1,11 +1,11 @@
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Image } from "~/components/Image";
import {
SendouSelect,
SendouSelectItem,
SendouSelectItemSection,
} from "~/components/elements/Select";
import { Image } from "~/components/Image";
import type { GearType } from "~/db/tables";
import { brandIds } from "~/modules/in-game-lists/brand-ids";
import {

View File

@@ -1,15 +1,18 @@
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import type { TierName } from "~/features/mmr/mmr-constants";
import type { MainWeaponId, StageId } from "~/modules/in-game-lists/types";
import type { ModeShortWithSpecial } from "~/modules/in-game-lists/types";
import type {
MainWeaponId,
ModeShortWithSpecial,
StageId,
} from "~/modules/in-game-lists/types";
import {
TIER_PLUS_URL,
mainWeaponImageUrl,
modeImageUrl,
outlinedFiveStarMainWeaponImageUrl,
outlinedMainWeaponImageUrl,
stageImageUrl,
TIER_PLUS_URL,
tierImageUrl,
} from "~/utils/urls";
@@ -45,6 +48,7 @@ export function Image({
loading,
}: ImageProps) {
return (
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<picture
title={title}
className={containerClassName}

View File

@@ -5,7 +5,10 @@ import { SendouPopover } from "./elements/Popover";
export function InfoPopover({
children,
tiny = false,
}: { children: React.ReactNode; tiny?: boolean }) {
}: {
children: React.ReactNode;
tiny?: boolean;
}) {
return (
<SendouPopover
trigger={

View File

@@ -50,7 +50,6 @@
}
.modeButton {
padding: 0;
padding: var(--s-1);
border: 2px solid var(--bg-darker);
border-radius: var(--rounded-full);

View File

@@ -29,6 +29,7 @@ export function Pagination({
/>
<div className="pagination__dots">
{nullFilledArray(pagesCount).map((_, i) => (
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<div
key={i}
className={clsx("pagination__dot", {

View File

@@ -1,5 +1,5 @@
import { NavLink } from "@remix-run/react";
import type { LinkProps } from "@remix-run/react";
import { NavLink } from "@remix-run/react";
import clsx from "clsx";
import type * as React from "react";

View File

@@ -1,8 +1,7 @@
import clsx from "clsx";
import { useRef, useState } from "react";
import * as React from "react";
import { Dialog } from "react-aria-components";
import { Popover } from "react-aria-components";
import { useRef, useState } from "react";
import { Dialog, Popover } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { useCopyToClipboard } from "react-use";
import { SendouButton } from "./elements/Button";

View File

@@ -2,20 +2,20 @@ import clsx from "clsx";
import * as React from "react";
import type { Key } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { Image, WeaponImage } from "~/components/Image";
import {
SendouSelect,
SendouSelectItem,
SendouSelectItemSection,
} from "~/components/elements/Select";
import { Image, WeaponImage } from "~/components/Image";
import type { AnyWeapon } from "~/features/build-analyzer";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { filterWeapon } from "~/modules/in-game-lists/utils";
import {
SPLAT_BOMB_ID,
TRIZOOKA_ID,
specialWeaponIds,
subWeaponIds,
TRIZOOKA_ID,
weaponCategories,
} from "~/modules/in-game-lists/weapon-ids";
import {

View File

@@ -42,8 +42,9 @@
padding: var(--s-6);
text-align: left;
vertical-align: middle;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px
rgba(0, 0, 0, 0.05);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
margin: auto;
}

View File

@@ -1,14 +1,13 @@
import { useNavigate } from "@remix-run/react";
import clsx from "clsx";
import type { ModalOverlayProps } from "react-aria-components";
import {
Dialog,
DialogTrigger,
Heading,
Modal,
ModalOverlay,
} from "react-aria-components";
import { Modal } from "react-aria-components";
import { useNavigate } from "@remix-run/react";
import clsx from "clsx";
import { SendouButton } from "~/components/elements/Button";
import { CrossIcon } from "~/components/icons/Cross";
import styles from "./Dialog.module.css";

View File

@@ -2,7 +2,9 @@ import { Text } from "react-aria-components";
export function SendouFieldMessage({
children,
}: { children: React.ReactNode }) {
}: {
children: React.ReactNode;
}) {
return (
<Text slot="description" className="info-message">
{children}

View File

@@ -59,6 +59,7 @@ export function SendouMenuItem(props: SendouMenuItemProps) {
})
}
>
{/** biome-ignore lint/complexity/noUselessFragments: Biome v2 migration */}
<>
{props.icon ? (
<span className={styles.itemIcon}>{props.icon}</span>

View File

@@ -72,7 +72,6 @@
.item {
font-size: var(--fonts-xsm);
font-weight: var(--semi-bold);
white-space: pre-wrap;
padding: var(--s-1-5);
border-radius: var(--rounded-sm);
height: 33px;

View File

@@ -16,8 +16,8 @@ import {
Select,
SelectStateContext,
SelectValue,
Virtualizer,
useFilter,
Virtualizer,
} from "react-aria-components";
import { useTranslation } from "react-i18next";
import { SendouBottomTexts } from "~/components/elements/BottomTexts";

View File

@@ -1,12 +1,14 @@
import clsx from "clsx";
import { Tabs, type TabsProps } from "react-aria-components";
import {
Tab,
TabList,
type TabListProps,
TabPanel,
type TabPanelProps,
type TabProps,
Tabs,
type TabsProps,
} from "react-aria-components";
import { Tab, type TabProps } from "react-aria-components";
import buttonStyles from "./Button.module.css";
import styles from "./Tabs.module.css";

View File

@@ -20,7 +20,6 @@
border: 2px solid var(--border);
width: 250px;
outline: none;
/* biome-ignore lint/correctness/noUnknownProperty: it exists */
view-transition-class: toast;
}

View File

@@ -1,7 +1,6 @@
.item {
font-size: var(--fonts-xsm);
font-weight: var(--semi-bold);
white-space: pre-wrap;
padding: var(--s-1-5);
border-radius: var(--rounded-sm);
height: 33px;

View File

@@ -2,6 +2,7 @@ import { useFetcher } from "@remix-run/react";
import clsx from "clsx";
import * as React from "react";
import {
Autocomplete,
Button,
Input,
type Key,
@@ -13,7 +14,6 @@ import {
type SelectProps,
SelectValue,
} from "react-aria-components";
import { Autocomplete } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { useDebounce } from "react-use";
import { SendouBottomTexts } from "~/components/elements/BottomTexts";
@@ -134,7 +134,6 @@ function UserItem({
if (typeof item.id === "string") {
return (
<ListBoxItem
id="PLACEHOLDER"
textValue="PLACEHOLDER"
isDisabled
className={userSearchStyles.placeholder}

View File

@@ -2,11 +2,7 @@ import { useTranslation } from "react-i18next";
import { SendouButton } from "../elements/Button";
import { PlusIcon } from "../icons/Plus";
export function AddFieldButton({
onClick,
}: {
onClick: () => void;
}) {
export function AddFieldButton({ onClick }: { onClick: () => void }) {
const { t } = useTranslation(["common"]);
return (

View File

@@ -5,7 +5,11 @@ export function FormFieldset({
title,
children,
onRemove,
}: { title: string; children: React.ReactNode; onRemove: () => void }) {
}: {
title: string;
children: React.ReactNode;
onRemove: () => void;
}) {
return (
<fieldset className="w-min">
<legend>{title}</legend>

View File

@@ -6,8 +6,8 @@ import { useTranslation } from "react-i18next";
import type { z } from "zod/v4";
import { logger } from "~/utils/logger";
import type { ActionError } from "~/utils/remix.server";
import { SubmitButton } from "../SubmitButton";
import { LinkButton } from "../elements/Button";
import { SubmitButton } from "../SubmitButton";
export function SendouForm<T extends z.ZodTypeAny>({
schema,

View File

@@ -14,7 +14,11 @@ export function ToggleFormField<T extends FieldValues>({
label,
name,
bottomText,
}: { label: string; name: FieldPath<T>; bottomText?: string }) {
}: {
label: string;
name: FieldPath<T>;
bottomText?: string;
}) {
const methods = useFormContext();
const id = React.useId();

View File

@@ -12,7 +12,11 @@ export function UserSearchFormField<T extends FieldValues>({
label,
name,
bottomText,
}: { label: string; name: FieldPath<T>; bottomText?: string }) {
}: {
label: string;
name: FieldPath<T>;
bottomText?: string;
}) {
const methods = useFormContext();
const error = get(methods.formState.errors, name);

View File

@@ -1,8 +1,4 @@
export function ArrowDownOnSquareIcon({
className,
}: {
className?: string;
}) {
export function ArrowDownOnSquareIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,10 @@
export function ArrowLongLeftIcon({
className,
title,
}: { className?: string; title?: string }) {
}: {
className?: string;
title?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,8 +1,4 @@
export function ArrowUpOnSquareIcon({
className,
}: {
className?: string;
}) {
export function ArrowUpOnSquareIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -2,7 +2,6 @@ export function BattlefyIcon() {
return (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"

View File

@@ -1,7 +1,10 @@
export function BeakerIcon({
className,
title,
}: { className?: string; title?: string }) {
}: {
className?: string;
title?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,10 @@
export function CrossIcon({
className,
title,
}: { className?: string; title?: string }) {
}: {
className?: string;
title?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,10 @@
export function EditIcon({
className,
title,
}: { className?: string; title?: string }) {
}: {
className?: string;
title?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,8 +1,4 @@
export function HamburgerIcon({
className,
}: {
className?: string;
}) {
export function HamburgerIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,10 @@
export function TrophyIcon({
className,
title,
}: { className?: string; title?: string }) {
}: {
className?: string;
title?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -4,15 +4,15 @@ import { useUser } from "~/features/auth/core/user";
import { FF_SCRIMS_ENABLED } from "~/features/scrims/scrims-constants";
import {
CALENDAR_NEW_PAGE,
NEW_TEAM_PAGE,
TOURNAMENT_NEW_PAGE,
lfgNewPostPage,
NEW_TEAM_PAGE,
navIconUrl,
newArtPage,
newAssociationsPage,
newScrimPostPage,
newVodPage,
plusSuggestionsNewPage,
TOURNAMENT_NEW_PAGE,
userNewBuildPage,
} from "~/utils/urls";
import {

View File

@@ -34,7 +34,7 @@ export function LogInButtonContainer({
>
<div className="stack md layout__user-item">
{authError === "aborted" ? (
<>{t("auth.errors.discordPermissions")}</>
t("auth.errors.discordPermissions")
) : (
<>
{t("auth.errors.unknown")}{" "}

View File

@@ -5,8 +5,8 @@ import { navItems } from "~/components/layout/nav-items";
import { useUser } from "~/features/auth/core/user";
import { LOG_OUT_URL, navIconUrl, userPage } from "~/utils/urls";
import { Avatar } from "../Avatar";
import { Image } from "../Image";
import { SendouButton } from "../elements/Button";
import { Image } from "../Image";
import { CrossIcon } from "../icons/Cross";
import { LogOutIcon } from "../icons/LogOut";
import { LogInButtonContainer } from "./LogInButtonContainer";
@@ -14,7 +14,10 @@ import { LogInButtonContainer } from "./LogInButtonContainer";
export function NavDialog({
isOpen,
close,
}: { isOpen: boolean; close: () => void }) {
}: {
isOpen: boolean;
close: () => void;
}) {
const user = useUser();
const { t } = useTranslation(["common"]);

View File

@@ -12,8 +12,7 @@ import { NOTIFICATIONS } from "~/features/notifications/notifications-contants";
import type { RootLoaderData } from "~/root";
import { NOTIFICATIONS_URL } from "~/utils/urls";
import { useMarkNotificationsAsSeen } from "../../features/notifications/notifications-hooks";
import { LinkButton } from "../elements/Button";
import { SendouButton } from "../elements/Button";
import { LinkButton, SendouButton } from "../elements/Button";
import { SendouPopover } from "../elements/Popover";
import { BellIcon } from "../icons/Bell";
import { RefreshIcon } from "../icons/Refresh";

View File

@@ -4,15 +4,14 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import type { RootLoaderData } from "~/root";
import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
import { Image } from "../Image";
import { SendouButton } from "../elements/Button";
import { Image } from "../Image";
import { HamburgerIcon } from "../icons/Hamburger";
import { Footer } from "./Footer";
import styles from "./index.module.css";
import { NavDialog } from "./NavDialog";
import { TopRightButtons } from "./TopRightButtons";
import styles from "./index.module.css";
function useBreadcrumbs() {
const { t } = useTranslation();
const matches = useMatches();

View File

@@ -14,19 +14,22 @@ import { MapPool } from "~/features/map-list-generator/core/map-pool";
import * as NotificationRepository from "~/features/notifications/NotificationRepository.server";
import type { Notification } from "~/features/notifications/notifications-types";
import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import {
lastCompletedVoting,
nextNonCompletedVoting,
rangeToMonthYear,
} from "~/features/plus-voting/core";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
import * as QMatchRepository from "~/features/sendouq-match/QMatchRepository.server";
import * as QRepository from "~/features/sendouq/QRepository.server";
import { addMember } from "~/features/sendouq/queries/addMember.server";
import { createMatch } from "~/features/sendouq/queries/createMatch.server";
import { calculateMatchSkills } from "~/features/sendouq-match/core/skills.server";
import {
summarizeMaps,
summarizePlayerResults,
} from "~/features/sendouq-match/core/summarizer.server";
import * as QMatchRepository from "~/features/sendouq-match/QMatchRepository.server";
import { winnersArrayToWinner } from "~/features/sendouq-match/q-match-utils";
import { addMapResults } from "~/features/sendouq-match/queries/addMapResults.server";
import { addPlayerResults } from "~/features/sendouq-match/queries/addPlayerResults.server";
@@ -35,15 +38,12 @@ import { addSkills } from "~/features/sendouq-match/queries/addSkills.server";
import { findMatchById } from "~/features/sendouq-match/queries/findMatchById.server";
import { reportScore } from "~/features/sendouq-match/queries/reportScore.server";
import { setGroupAsInactive } from "~/features/sendouq-match/queries/setGroupAsInactive.server";
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "~/features/sendouq-settings/q-settings-constants";
import * as QRepository from "~/features/sendouq/QRepository.server";
import { addMember } from "~/features/sendouq/queries/addMember.server";
import { createMatch } from "~/features/sendouq/queries/createMatch.server";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { createVod } from "~/features/vods/queries/createVod.server";
import {
@@ -56,8 +56,7 @@ import {
headGearIds,
shoesGearIds,
} from "~/modules/in-game-lists/gear-ids";
import { modesShort } from "~/modules/in-game-lists/modes";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
import { stageIds } from "~/modules/in-game-lists/stage-ids";
import type {
AbilityType,
@@ -181,12 +180,9 @@ const basicSeeds = (variation?: SeedVariation | null) => [
export async function seed(variation?: SeedVariation | null) {
wipeDB();
let count = 0;
for (const seedFunc of basicSeeds(variation)) {
if (!seedFunc) continue;
count++;
faker.seed(5800);
await seedFunc();
@@ -525,7 +521,7 @@ async function lastMonthsVoting() {
const { month, year } = lastCompletedVoting(new Date());
const fiveMinutesAgo = new Date(new Date().getTime() - 5 * 60 * 1000);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
for (let i = 1; i < 151; i++) {
if (i === NZAP_TEST_ID) continue; // omit N-ZAP user for testing;

View File

@@ -8,6 +8,7 @@ import {
} from "kysely";
import { format } from "sql-formatter";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import { roundToNDecimalPlaces } from "~/utils/number";
import type { DB } from "./tables";
@@ -43,16 +44,16 @@ function logQuery(event: LogEvent) {
(event.query.query as any).from.froms.map(
(f: any) => f.table.identifier.name,
);
// biome-ignore lint/suspicious/noConsoleLog: dev only
// biome-ignore lint/suspicious/noConsole: dev only
console.log(styleText("blue", `-- SQLITE QUERY to "${from()}" --`));
// biome-ignore lint/suspicious/noConsoleLog: dev only
// biome-ignore lint/suspicious/noConsole: dev only
console.log(
styleText(
millisToColor(event.queryDurationMillis),
`${roundToNDecimalPlaces(event.queryDurationMillis, 1)}ms`,
),
);
// biome-ignore lint/suspicious/noConsoleLog: dev only
// biome-ignore lint/suspicious/noConsole: dev only
console.log(formatSql(event.query.sql, event.query.parameters));
} else {
logError(event);
@@ -70,7 +71,7 @@ function logError(event: LogEvent) {
// btw this particular check is here just to avoid the double "no transaction is active" log
!(event.error as any).message.includes("no transaction is active")
) {
console.error(event.error);
logger.error(event.error);
}
}

View File

@@ -3,6 +3,7 @@ import i18next from "i18next";
import { hydrateRoot } from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import { i18nLoader } from "./modules/i18n/loader";
import { logger } from "./utils/logger";
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
@@ -20,4 +21,4 @@ i18nLoader()
</I18nextProvider>,
),
)
.catch(console.error);
.catch(logger.error);

View File

@@ -1,7 +1,7 @@
import { PassThrough } from "node:stream";
import {
type EntryContext,
createReadableStreamFromReadable,
type EntryContext,
} from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { createInstance } from "i18next";
@@ -13,6 +13,7 @@ import { config } from "~/modules/i18n/config"; // your i18n configuration file
import i18next from "~/modules/i18n/i18next.server";
import { resources } from "./modules/i18n/resources.server";
import { daily, everyHourAt00, everyHourAt30 } from "./routines/list.server";
import { logger } from "./utils/logger";
const ABORT_DELAY = 5000;
@@ -67,7 +68,7 @@ export default async function handleRequest(
onError(error: unknown) {
didError = true;
console.error(error);
logger.error(error);
},
},
);
@@ -105,5 +106,5 @@ if (!global.appStartSignal && process.env.NODE_ENV === "production") {
}
process.on("unhandledRejection", (reason: string, p: Promise<any>) => {
console.error("Unhandled Rejection at:", p, "reason:", reason);
logger.error("Unhandled Rejection at:", p, "reason:", reason);
});

View File

@@ -1,8 +1,8 @@
import { addPendingPlusTiers } from "~/features/leaderboards/core/leaderboards.server";
import { userSPLeaderboard } from "~/features/leaderboards/queries/userSPLeaderboard.server";
import * as Seasons from "~/features/mmr/core/Seasons";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import { seasonToVotingRange } from "~/features/plus-voting/core/voting-time";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import invariant from "~/utils/invariant";
import { userIsBanned } from "../../ban/core/banned.server";

View File

@@ -3,13 +3,13 @@ import { db } from "~/db/sql";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
import * as TeamRepository from "~/features/team/TeamRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import {
assertResponseErrored,
dbInsertUsers,
dbReset,
wrappedAction,
} from "~/utils/Test";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import type { adminActionSchema } from "../actions/admin.server";
import { action } from "./admin";

View File

@@ -10,9 +10,6 @@ import {
import * as React from "react";
import { Avatar } from "~/components/Avatar";
import { Catcher } from "~/components/Catcher";
import { Input } from "~/components/Input";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import { SendouButton } from "~/components/elements/Button";
import {
SendouTab,
@@ -21,14 +18,17 @@ import {
SendouTabs,
} from "~/components/elements/Tabs";
import { UserSearch } from "~/components/elements/UserSearch";
import { Input } from "~/components/Input";
import { SearchIcon } from "~/components/icons/Search";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import { FRIEND_CODE_REGEXP_PATTERN } from "~/features/sendouq/q-constants";
import { useHasRole } from "~/modules/permissions/hooks";
import { metaTags } from "~/utils/remix";
import {
impersonateUrl,
SEED_URL,
STOP_IMPERSONATING_URL,
impersonateUrl,
userPage,
} from "~/utils/urls";
@@ -274,6 +274,7 @@ function GiveTournamentOrganizer() {
function UpdateFriendCode() {
const fetcher = useFetcher();
const id = React.useId();
return (
<fetcher.Form className="stack md" method="post">
@@ -281,10 +282,10 @@ function UpdateFriendCode() {
<div className="stack horizontal md">
<UserSearch label="User" name="user" />
<div>
<label>Friend code</label>
<label htmlFor={id}>Friend code</label>
<Input
leftAddon="SW-"
id="friendCode"
id={id}
name="friendCode"
pattern={FRIEND_CODE_REGEXP_PATTERN}
placeholder="1234-5678-9012"

View File

@@ -94,7 +94,10 @@ function SendouQSeason() {
function InfoSquare({
title,
children,
}: { title: string; children: React.ReactNode }) {
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div className="font-semi-bold">
{title}
@@ -122,7 +125,11 @@ function EventSchedule({
name,
date,
imgId,
}: { name: string; date: Date; imgId: string }) {
}: {
name: string;
date: Date;
imgId: string;
}) {
return (
<div className="stack md horizontal items-center mx-auto">
<img

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { findCurrentGroupByUserId } from "~/features/sendouq/queries/findCurrentGroupByUserId.server";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import * as QMatchRepository from "~/features/sendouq-match/QMatchRepository.server";

View File

@@ -1,11 +1,11 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
import { resolveMapList } from "~/features/tournament-bracket/core/mapList.server";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import { resolveMapList } from "~/features/tournament-bracket/core/mapList.server";
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
import i18next from "~/modules/i18n/i18next.server";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { id } from "~/utils/zod";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import type { Bracket } from "~/features/tournament-bracket/core/Bracket";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, json } from "@remix-run/node";
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";

View File

@@ -3,15 +3,14 @@ import clsx from "clsx";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Avatar } from "~/components/Avatar";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { Pagination } from "~/components/Pagination";
import { LinkButton } from "~/components/elements/Button";
import { SendouButton } from "~/components/elements/Button";
import { LinkButton, SendouButton } from "~/components/elements/Button";
import { SendouDialog } from "~/components/elements/Dialog";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { CrossIcon } from "~/components/icons/Cross";
import { EditIcon } from "~/components/icons/Edit";
import { TrashIcon } from "~/components/icons/Trash";
import { UnlinkIcon } from "~/components/icons/Unlink";
import { Pagination } from "~/components/Pagination";
import { useIsMounted } from "~/hooks/useIsMounted";
import { usePagination } from "~/hooks/usePagination";
import { useSearchParamState } from "~/hooks/useSearchParamState";
@@ -170,6 +169,7 @@ function ImagePreview({
const { t } = useTranslation(["common", "art"]);
const img = (
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<img
alt=""
src={conditionalUserSubmittedImage(previewUrl(art.url))}

View File

@@ -6,15 +6,16 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useFetcher } from "react-router-dom";
import { Alert } from "~/components/Alert";
import { FormMessage } from "~/components/FormMessage";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { SendouButton } from "~/components/elements/Button";
import { SendouSwitch } from "~/components/elements/Switch";
import { UserSearch } from "~/components/elements/UserSearch";
import { FormMessage } from "~/components/FormMessage";
import { CrossIcon } from "~/components/icons/Cross";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { useHasRole } from "~/modules/permissions/hooks";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
artPage,
@@ -22,11 +23,10 @@ import {
navIconUrl,
} from "~/utils/urls";
import { metaTitle } from "../../../utils/remix";
import { action } from "../actions/art.new.server";
import { ART } from "../art-constants";
import { previewUrl } from "../art-utils";
import { TagSelect } from "../components/TagSelect";
import { action } from "../actions/art.new.server";
import { loader } from "../loaders/art.new.server";
export { loader, action };
@@ -113,6 +113,7 @@ function ImageUpload({
}) {
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["common"]);
const id = React.useId();
if (data.art) {
return (
@@ -125,9 +126,9 @@ function ImageUpload({
return (
<div>
<label htmlFor="img-field">{t("common:upload.imageToUpload")}</label>
<label htmlFor={id}>{t("common:upload.imageToUpload")}</label>
<input
id="img-field"
id={id}
className="plain"
type="file"
name="img"
@@ -147,7 +148,7 @@ function ImageUpload({
setImg(file);
},
error(err) {
console.error(err.message);
logger.error(err.message);
},
});
@@ -160,7 +161,7 @@ function ImageUpload({
setSmallImg(file);
},
error(err) {
console.error(err.message);
logger.error(err.message);
},
});
}}
@@ -174,17 +175,18 @@ function Description() {
const { t } = useTranslation(["art"]);
const data = useLoaderData<typeof loader>();
const [value, setValue] = React.useState(data.art?.description ?? "");
const id = React.useId();
return (
<div>
<Label
htmlFor="description"
htmlFor={id}
valueLimits={{ current: value.length, max: ART.DESCRIPTION_MAX_LENGTH }}
>
{t("art:forms.description.title")}
</Label>
<textarea
id="description"
id={id}
name="description"
value={value}
onChange={(e) => setValue(e.target.value)}
@@ -384,15 +386,16 @@ function ShowcaseToggle() {
const data = useLoaderData<typeof loader>();
const isCurrentlyShowcase = Boolean(data.art?.isShowcase);
const [checked, setChecked] = React.useState(isCurrentlyShowcase);
const id = React.useId();
return (
<div>
<label htmlFor="isShowcase">{t("art:forms.showcase.title")}</label>
<label htmlFor={id}>{t("art:forms.showcase.title")}</label>
<SendouSwitch
isSelected={checked}
onChange={setChecked}
name="isShowcase"
id="isShowcase"
id={id}
isDisabled={isCurrentlyShowcase}
/>
<FormMessage type="info">{t("art:forms.showcase.info")}</FormMessage>

View File

@@ -1,13 +1,14 @@
import type { MetaFunction, SerializeFrom } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { SendouButton } from "~/components/elements/Button";
import { SendouSwitch } from "~/components/elements/Switch";
import { CrossIcon } from "~/components/icons/Cross";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { artPage, navIconUrl, newArtPage } from "~/utils/urls";
import { metaTags } from "../../../utils/remix";
@@ -60,6 +61,7 @@ export default function ArtPage() {
const { t } = useTranslation(["art", "common"]);
const data = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const switchId = React.useId();
const filteredTag = searchParams.get(FILTERED_TAG_KEY_SEARCH_PARAM_KEY);
const showOpenCommissions = searchParams.get(OPEN_COMMISIONS_KEY) === "true";
@@ -80,9 +82,9 @@ export default function ArtPage() {
return prev;
})
}
id="open"
id={switchId}
/>
<Label htmlFor="open" className="m-auto-0">
<Label htmlFor={switchId} className="m-auto-0">
{t("art:openCommissionsOnly")}
</Label>
</div>

View File

@@ -162,7 +162,10 @@ export function refreshInviteCode(associationId: number) {
export function addMember({
associationId,
userId,
}: { associationId: number; userId: number }) {
}: {
associationId: number;
userId: number;
}) {
return db
.insertInto("AssociationMember")
.values({ associationId, userId, role: "MEMBER" })
@@ -172,7 +175,10 @@ export function addMember({
export function removeMember({
associationId,
userId,
}: { associationId: number; userId: number }) {
}: {
associationId: number;
userId: number;
}) {
return db
.deleteFrom("AssociationMember")
.where("associationId", "=", associationId)

View File

@@ -121,7 +121,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
async function validateHasManagePermissions({
user,
associationId,
}: { user: { id: number }; associationId: number }) {
}: {
user: { id: number };
associationId: number;
}) {
const association = badRequestIfFalsy(
await AssociationRepository.findById(associationId, { withMembers: true }),
);

View File

@@ -4,24 +4,23 @@ import { useTranslation } from "react-i18next";
import { useCopyToClipboard } from "react-use";
import { AddNewButton } from "~/components/AddNewButton";
import { Avatar } from "~/components/Avatar";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import { SendouButton } from "~/components/elements/Button";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { CheckmarkIcon } from "~/components/icons/Checkmark";
import { ClipboardIcon } from "~/components/icons/Clipboard";
import { TrashIcon } from "~/components/icons/Trash";
import { useUser } from "~/features/auth/core/user";
import { useHasPermission } from "~/modules/permissions/hooks";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { associationsPage, newAssociationsPage, userPage } from "~/utils/urls";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import { action } from "~/features/associations/actions/associations.server";
import {
type AssociationsLoaderData,
loader,
} from "~/features/associations/loaders/associations.server";
import { useUser } from "~/features/auth/core/user";
import { useHasPermission } from "~/modules/permissions/hooks";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { associationsPage, newAssociationsPage, userPage } from "~/utils/urls";
export { loader, action };
export const handle: SendouRouteHandle = {
@@ -89,7 +88,9 @@ function JoinForm() {
function Association({
association,
}: { association: AssociationsLoaderData["associations"][number] }) {
}: {
association: AssociationsLoaderData["associations"][number];
}) {
const { t } = useTranslation(["common", "scrims"]);
const user = useUser();
const canManage = useHasPermission(association, "MANAGE");
@@ -169,11 +170,15 @@ function Association({
function AssociationInviteCodeActions({
associationId,
inviteCode,
}: { associationId: number; inviteCode: string }) {
}: {
associationId: number;
inviteCode: string;
}) {
const { t } = useTranslation(["common", "scrims"]);
const [state, copyToClipboard] = useCopyToClipboard();
const [copySuccess, setCopySuccess] = React.useState(false);
const fetcher = useFetcher();
const id = React.useId();
React.useEffect(() => {
if (!state.value) return;
@@ -188,9 +193,9 @@ function AssociationInviteCodeActions({
return (
<div className="mt-6">
<label htmlFor="invite">{t("scrims:associations.shareLink.title")}</label>
<label htmlFor={id}>{t("scrims:associations.shareLink.title")}</label>
<div className="stack horizontal sm items-center">
<input type="text" value={inviteLink} readOnly id="invite" />
<input type="text" value={inviteLink} readOnly id={id} />
<SendouButton
variant={copySuccess ? "outlined-success" : "outlined"}
onPress={() => copyToClipboard(inviteLink)}

View File

@@ -91,7 +91,7 @@ export const DiscordStrategy = () => {
return userFromDb.id;
} catch (e) {
console.error("Failed to finish authentication:\n", e);
logger.error("Failed to finish authentication:\n", e);
throw new Error("Failed to finish authentication");
}
},

View File

@@ -15,9 +15,9 @@ import { createLogInLink } from "../queries/createLogInLink.server";
import { deleteLogInLinkByCode } from "../queries/deleteLogInLinkByCode.server";
import { userIdByLogInLinkCode } from "../queries/userIdByLogInLinkCode.server";
import {
authenticator,
IMPERSONATED_SESSION_KEY,
SESSION_KEY,
authenticator,
} from "./authenticator.server";
import { authSessionStorage } from "./session.server";
import { getUserId, requireUser } from "./user.server";

View File

@@ -8,10 +8,9 @@ import { TrashIcon } from "~/components/icons/Trash";
import type { Tables } from "~/db/tables";
import { useHasPermission, useHasRole } from "~/modules/permissions/hooks";
import { atOrError } from "~/utils/arrays";
import { action } from "../actions/badges.$id.edit.server";
import type { BadgeDetailsLoaderData } from "../loaders/badges.$id.server";
import type { BadgeDetailsContext } from "./badges.$id";
import { action } from "../actions/badges.$id.edit.server";
export { action };
export default function EditBadgePage() {
@@ -148,7 +147,6 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
{owner.username}
<input
className="badges-edit__number-input"
id="number"
type="number"
value={owner.count}
min={0}

View File

@@ -5,8 +5,8 @@ import { useTranslation } from "react-i18next";
import { Badge } from "~/components/Badge";
import { Divider } from "~/components/Divider";
import { Input } from "~/components/Input";
import { Main } from "~/components/Main";
import { SearchIcon } from "~/components/icons/Search";
import { Main } from "~/components/Main";
import { useUser } from "~/features/auth/core/user";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { BADGES_DOC_LINK, BADGES_PAGE, navIconUrl } from "~/utils/urls";

View File

@@ -10,7 +10,7 @@ import { isAbility } from "~/modules/in-game-lists/utils";
import invariant from "~/utils/invariant";
import { MAX_LDE_INTENSITY } from "./analyzer-constants";
import type { SpecialEffectType } from "./analyzer-types";
import { SPECIAL_EFFECTS, applySpecialEffects } from "./core/specialEffects";
import { applySpecialEffects, SPECIAL_EFFECTS } from "./core/specialEffects";
import { buildStats } from "./core/stats";
import {
buildIsEmpty,

View File

@@ -1,10 +1,10 @@
import type {
Ability,
AbilityWithUnknown,
MainWeaponId,
SpecialWeaponId,
SubWeaponId,
} from "~/modules/in-game-lists/types";
import type { AbilityWithUnknown } from "~/modules/in-game-lists/types";
import type { DAMAGE_TYPE } from "./analyzer-constants";
import type { SPECIAL_EFFECTS } from "./core/specialEffects";
import type { abilityValues } from "./core/utils";

View File

@@ -90,6 +90,7 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
);
})}
</div>
{/** biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration */}
<div className="stack horizontal sm" onMouseLeave={handleMouseLeaveGrid}>
<div className="analyzer__ink-grid__horizontal-ability">
<Ability ability="ISS" size="SUBTINY" />
@@ -134,6 +135,7 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
if (typeof cell.shots !== "number") {
return (
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<div
className="analyzer__ink-grid__cell"
key={key}
@@ -149,6 +151,7 @@ function Grid({ weaponSplId }: PerInkTankGridProps) {
}
return (
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<div
key={key}
className="analyzer__ink-grid__cell"

View File

@@ -18,17 +18,17 @@ import {
SPLAT_BOMB_ID,
SPRINKLER_ID,
SUCTION_BOMB_ID,
subWeaponIds,
TORPEDO_ID,
ZIPCASTER_ID,
subWeaponIds,
} from "~/modules/in-game-lists/weapon-ids";
import invariant from "~/utils/invariant";
import { cutToNDecimalPlaces, roundToNDecimalPlaces } from "~/utils/number";
import { assertUnreachable } from "~/utils/types";
import {
DAMAGE_TYPE,
RAINMAKER_SPEED_PENALTY_MODIFIER,
multiShot,
RAINMAKER_SPEED_PENALTY_MODIFIER,
} from "../analyzer-constants";
import type {
AbilityPoints,

View File

@@ -293,7 +293,7 @@ export function validatedBuildFromSearchParams(
validateAbility(["STACKABLE"], abilitiesArr[11]),
],
];
} catch (err) {
} catch {
return EMPTY_BUILD;
}
}

View File

@@ -1,19 +1,11 @@
export {
possibleApValues,
validatedWeaponIdFromSearchParams,
validatedBuildFromSearchParams,
serializeBuild,
hpDivided,
validatedAnyWeaponFromSearchParams,
buildToAbilityPoints,
} from "./core/utils";
export { DAMAGE_TYPE, damageTypeToWeaponType } from "./analyzer-constants";
export type {
DamageType,
AbilityPoints,
AnalyzedBuild,
AnyWeapon,
DamageType,
SpecialWeaponParams,
SubWeaponParams,
AnyWeapon,
} from "./analyzer-types";
export {
buildStats,
@@ -21,4 +13,12 @@ export {
specialFieldHp,
subStats,
} from "./core/stats";
export { DAMAGE_TYPE, damageTypeToWeaponType } from "./analyzer-constants";
export {
buildToAbilityPoints,
hpDivided,
possibleApValues,
serializeBuild,
validatedAnyWeaponFromSearchParams,
validatedBuildFromSearchParams,
validatedWeaponIdFromSearchParams,
} from "./core/utils";

View File

@@ -7,21 +7,21 @@ import { useTranslation } from "react-i18next";
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
import { Ability } from "~/components/Ability";
import Chart from "~/components/Chart";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { Table } from "~/components/Table";
import {
SendouTab,
SendouTabList,
SendouTabPanel,
SendouTabs,
} from "~/components/elements/Tabs";
import { Image } from "~/components/Image";
import { BeakerIcon } from "~/components/icons/Beaker";
import { Main } from "~/components/Main";
import { Table } from "~/components/Table";
import { useUser } from "~/features/auth/core/user";
import { useIsMounted } from "~/hooks/useIsMounted";
import { abilitiesShort } from "~/modules/in-game-lists/abilities";
import type { Ability as AbilityType } from "~/modules/in-game-lists/types";
import type {
Ability as AbilityType,
BuildAbilitiesTupleWithUnknown,
MainWeaponId,
SubWeaponId,
@@ -53,9 +53,9 @@ import { SendouButton } from "../../../components/elements/Button";
import { SendouPopover } from "../../../components/elements/Popover";
import { metaTags } from "../../../utils/remix";
import {
damageTypeToWeaponType,
MAX_AP,
MAX_LDE_INTENSITY,
damageTypeToWeaponType,
} from "../analyzer-constants";
import { useAnalyzeBuild } from "../analyzer-hooks";
import type {
@@ -73,8 +73,8 @@ import {
getAbilityChunksMapAsArray,
} from "../core/abilityChunksCalc";
import {
SPECIAL_EFFECTS,
lastDitchEffortIntensityToAp,
SPECIAL_EFFECTS,
} from "../core/specialEffects";
import { buildStats } from "../core/stats";
import {
@@ -85,8 +85,9 @@ import {
} from "../core/utils";
import "../analyzer.css";
import * as R from "remeda";
import { WeaponSelect } from "~/components/WeaponSelect";
import { SendouSwitch } from "~/components/elements/Switch";
import { WeaponSelect } from "~/components/WeaponSelect";
import { logger } from "~/utils/logger";
export const CURRENT_PATCH = "10.0";
@@ -1039,7 +1040,7 @@ function StatChart({
// prevent crash but this should not happen
if (chartOptions.length === 0) {
console.error("no chart options");
logger.error("no chart options");
return null;
}

View File

@@ -1,7 +1,7 @@
import { cachified } from "@epic-web/cachified";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { i18next } from "~/modules/i18n/i18next.server";
import { IN_MILLISECONDS, cache, ttl } from "~/utils/cache.server";
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import { notFoundIfNullLike } from "~/utils/remix.server";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
import { popularBuilds } from "../build-stats-utils";

View File

@@ -1,7 +1,7 @@
import { cachified } from "@epic-web/cachified";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { i18next } from "~/modules/i18n/i18next.server";
import { IN_MILLISECONDS, cache, ttl } from "~/utils/cache.server";
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import { notFoundIfNullLike } from "~/utils/remix.server";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
import { abilityPointCountsToAverages } from "../build-stats-utils";

View File

@@ -1,14 +1,16 @@
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Ability } from "~/components/Ability";
import { ModeImage } from "~/components/Image";
import { SendouButton } from "~/components/elements/Button";
import { ModeImage } from "~/components/Image";
import { CrossIcon } from "~/components/icons/Cross";
import { possibleApValues } from "~/features/build-analyzer";
import { abilities } from "~/modules/in-game-lists/abilities";
import { modesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort } from "~/modules/in-game-lists/types";
import type { Ability as AbilityType } from "~/modules/in-game-lists/types";
import type {
Ability as AbilityType,
ModeShort,
} from "~/modules/in-game-lists/types";
import { dateToYYYYMMDD } from "~/utils/dates";
import { PATCHES } from "../builds-constants";
import type {

View File

@@ -1,5 +1,7 @@
import { abilities } from "~/modules/in-game-lists/abilities";
import { mainOnlyAbilitiesShort } from "~/modules/in-game-lists/abilities";
import {
abilities,
mainOnlyAbilitiesShort,
} from "~/modules/in-game-lists/abilities";
import type {
Ability,
BuildAbilitiesTuple,

View File

@@ -1,6 +1,7 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { i18next } from "~/modules/i18n/i18next.server";
import { weaponIdIsNotAlt } from "~/modules/in-game-lists/weapon-ids";
import { logger } from "~/utils/logger";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
import { mySlugify } from "~/utils/urls";
import {
@@ -38,7 +39,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const filters = buildFiltersSearchParams.safeParse(rawFilters ?? "[]");
if (!filters.success) {
console.error(
logger.error(
"Invalid filters",
JSON.stringify(filters.error.issues, null, 2),
);

View File

@@ -1,9 +1,9 @@
import { sql } from "~/db/sql";
import type { Tables, UserWithPlusTier } from "~/db/tables";
import type { ModeShort } from "~/modules/in-game-lists/types";
import type {
BuildAbilitiesTuple,
MainWeaponId,
ModeShort,
} from "~/modules/in-game-lists/types";
import { weaponIdToAltId } from "~/modules/in-game-lists/weapon-ids";
import invariant from "~/utils/invariant";

View File

@@ -7,10 +7,9 @@ import {
import { nanoid } from "nanoid";
import * as React from "react";
import { useTranslation } from "react-i18next";
import * as R from "remeda";
import { BuildCard } from "~/components/BuildCard";
import { Main } from "~/components/Main";
import { LinkButton } from "~/components/elements/Button";
import { SendouButton } from "~/components/elements/Button";
import { LinkButton, SendouButton } from "~/components/elements/Button";
import { SendouMenu, SendouMenuItem } from "~/components/elements/Menu";
import { BeakerFilledIcon } from "~/components/icons/BeakerFilled";
import { CalendarIcon } from "~/components/icons/Calendar";
@@ -18,6 +17,7 @@ import { ChartBarIcon } from "~/components/icons/ChartBar";
import { FilterIcon } from "~/components/icons/Filter";
import { FireIcon } from "~/components/icons/Fire";
import { MapIcon } from "~/components/icons/Map";
import { Main } from "~/components/Main";
import { useUser } from "~/features/auth/core/user";
import { safeJSONParse } from "~/utils/json";
import { isRevalidation, metaTags } from "~/utils/remix";
@@ -159,11 +159,7 @@ export const handle: SendouRouteHandle = {
},
};
export function BuildCards({
data,
}: {
data: SerializeFrom<typeof loader>;
}) {
export function BuildCards({ data }: { data: SerializeFrom<typeof loader> }) {
const user = useUser();
return (
@@ -194,8 +190,7 @@ export default function WeaponsBuildsPage() {
const filtersForSearchParams = (filters: BuildFilter[]) =>
JSON.stringify(
filters.map((f) => {
const { id, ...rest } = f;
return rest;
return R.omit(f, ["id"]);
}),
);
const syncSearchParams = (newFilters: BuildFilter[]) => {

View File

@@ -25,8 +25,7 @@ import {
} from "~/utils/remix.server";
import { calendarEventPage } from "~/utils/urls";
import { CALENDAR_EVENT } from "../calendar-constants";
import { canEditCalendarEvent } from "../calendar-utils";
import { regClosesAtDate } from "../calendar-utils";
import { canEditCalendarEvent, regClosesAtDate } from "../calendar-utils";
export const action: ActionFunction = async ({ request }) => {
const user = await requireUser(request);

View File

@@ -1,7 +1,7 @@
import { z } from "zod/v4";
import { type CalendarEventTag, TOURNAMENT_STAGE_TYPES } from "~/db/tables";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import "~/styles/calendar-new.css";
import { gamesShort, versusShort } from "~/modules/in-game-lists/games";
import { modesShortWithSpecial } from "~/modules/in-game-lists/modes";

View File

@@ -193,8 +193,7 @@ function eventStartedInThePast(
startTimes: CanReportCalendarEventWinnersArgs["startTimes"],
) {
return startTimes.every(
(startTime) =>
databaseTimestampToDate(startTime).getTime() < new Date().getTime(),
(startTime) => databaseTimestampToDate(startTime).getTime() < Date.now(),
);
}

View File

@@ -2,14 +2,14 @@ import { nanoid } from "nanoid";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { DateInput } from "~/components/DateInput";
import { FormMessage } from "~/components/FormMessage";
import { Input } from "~/components/Input";
import { Label } from "~/components/Label";
import { SendouButton } from "~/components/elements/Button";
import { SendouSwitch } from "~/components/elements/Switch";
import { FormMessage } from "~/components/FormMessage";
import { Input } from "~/components/Input";
import { PlusIcon } from "~/components/icons/Plus";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import { Label } from "~/components/Label";
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
import * as Progression from "~/features/tournament-bracket/core/Progression";
import { defaultBracketSettings } from "../../tournament/tournament-utils";
const defaultBracket = (): Progression.InputBracket => ({
@@ -395,11 +395,9 @@ function TournamentFormatBracketSelector({
) : null}
{!bracket.sources ? (
<FormMessage type="info">
{isInvitationalTournament ? (
<>Participants added by the organizer</>
) : (
<>Participants join from sign-up</>
)}
{isInvitationalTournament
? "Participants added by the organizer"
: "Participants join from sign-up"}
</FormMessage>
) : (
<SourcesSelector

View File

@@ -3,7 +3,6 @@ import { useFetcher, useSearchParams } from "@remix-run/react";
import * as React from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { SubmitButton } from "~/components/SubmitButton";
import { SendouButton } from "~/components/elements/Button";
import { SendouDialog } from "~/components/elements/Dialog";
import { InputFormField } from "~/components/form/InputFormField";
@@ -11,6 +10,7 @@ import { InputGroupFormField } from "~/components/form/InputGroupFormField";
import { TextArrayFormField } from "~/components/form/TextArrayFormField";
import { ToggleFormField } from "~/components/form/ToggleFormField";
import { FilterFilledIcon } from "~/components/icons/FilterFilled";
import { SubmitButton } from "~/components/SubmitButton";
import type { CalendarEventTag } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import { calendarFiltersFormSchema } from "~/features/calendar/calendar-schemas";
@@ -62,7 +62,10 @@ const TAGS_TO_OMIT: Array<CalendarEventTag> = [
function FiltersForm({
filters,
closeDialog,
}: { filters: CalendarFilters; closeDialog: () => void }) {
}: {
filters: CalendarFilters;
closeDialog: () => void;
}) {
const user = useUser();
const { t } = useTranslation(["game-misc", "calendar"]);
const methods = useForm({

View File

@@ -22,10 +22,6 @@
max-width: var(--card-width);
}
.card:hover .avatarImg {
outline: 6px solid var(--theme-transparent);
}
.imgContainer {
background-color: var(--bg);
padding: var(--s-1-5);
@@ -38,6 +34,10 @@
border-radius: 100%;
}
.card:hover .avatarImg {
outline: 6px solid var(--theme-transparent);
}
.org {
display: flex;
gap: var(--s-1);

View File

@@ -1,10 +1,10 @@
import { Link } from "@remix-run/react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Flag } from "~/components/Flag";
import { Image, ModeImage } from "~/components/Image";
import { SendouButton } from "~/components/elements/Button";
import { SendouPopover } from "~/components/elements/Popover";
import { Flag } from "~/components/Flag";
import { Image, ModeImage } from "~/components/Image";
import { TrophyIcon } from "~/components/icons/Trophy";
import { UsersIcon } from "~/components/icons/Users";
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
@@ -187,7 +187,9 @@ function ModesPill({ modes }: { modes: NonNullable<CalendarEvent["modes"]> }) {
function BadgePrizesPill({
badges,
}: { badges: NonNullable<CalendarEvent["badges"]> }) {
}: {
badges: NonNullable<CalendarEvent["badges"]>;
}) {
return (
<SendouPopover
trigger={

View File

@@ -9,7 +9,7 @@ function makeEvent(
overrides: Partial<CalendarEventType> = {},
): CalendarEventType {
return {
at: new Date().getTime(),
at: Date.now(),
id: 1,
isRanked: null,
tags: [],

View File

@@ -298,6 +298,9 @@ function matchesFilter(
function orgNameMatches({
orgName,
value,
}: { orgName: string; value: string }) {
}: {
orgName: string;
value: string;
}) {
return orgName.trim().toLowerCase() === value.trim().toLowerCase();
}

Some files were not shown because too many files have changed in this diff Show More