mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 14:16:04 -05:00
BIome v2 upgrade
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, ""];
|
||||
});
|
||||
|
||||
@@ -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[] }>();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
}
|
||||
|
||||
.modeButton {
|
||||
padding: 0;
|
||||
padding: var(--s-1);
|
||||
border: 2px solid var(--bg-darker);
|
||||
border-radius: var(--rounded-full);
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
border: 2px solid var(--border);
|
||||
width: 250px;
|
||||
outline: none;
|
||||
/* biome-ignore lint/correctness/noUnknownProperty: it exists */
|
||||
view-transition-class: toast;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")}{" "}
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -293,7 +293,7 @@ export function validatedBuildFromSearchParams(
|
||||
validateAbility(["STACKABLE"], abilitiesArr[11]),
|
||||
],
|
||||
];
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return EMPTY_BUILD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -9,7 +9,7 @@ function makeEvent(
|
||||
overrides: Partial<CalendarEventType> = {},
|
||||
): CalendarEventType {
|
||||
return {
|
||||
at: new Date().getTime(),
|
||||
at: Date.now(),
|
||||
id: 1,
|
||||
isRanked: null,
|
||||
tags: [],
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user