mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Migrate Comboboxes to react-aria-components (#2408)
* Initial * wip * AnyWeapon * del * wip * import stuff * gearselect * brand images * wip * wip * art * Remove old * Fix tournament map pool link * Simplify GearSelect * convert to todo
This commit is contained in:
parent
4e6951e36b
commit
7ed1394fba
|
|
@ -1,452 +0,0 @@
|
|||
import { Combobox as HeadlessCombobox } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import Fuse, { type IFuseOptions } from "fuse.js";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { GearType } from "~/db/tables";
|
||||
import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events";
|
||||
import { useAllEventsWithMapPools } from "~/hooks/swr";
|
||||
import {
|
||||
clothesGearIds,
|
||||
headGearIds,
|
||||
shoesGearIds,
|
||||
} from "~/modules/in-game-lists/gear-ids";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { weaponAltNames } from "~/modules/in-game-lists/weapon-alt-names";
|
||||
import {
|
||||
mainWeaponIds,
|
||||
subWeaponIds,
|
||||
weaponCategories,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import {
|
||||
nonBombSubWeaponIds,
|
||||
nonDamagingSpecialWeaponIds,
|
||||
specialWeaponIds,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import {
|
||||
gearImageUrl,
|
||||
mainWeaponImageUrl,
|
||||
specialWeaponImageUrl,
|
||||
subWeaponImageUrl,
|
||||
} from "~/utils/urls";
|
||||
import { Image } from "./Image";
|
||||
|
||||
const MAX_RESULTS_SHOWN = 6;
|
||||
|
||||
interface ComboboxBaseOption {
|
||||
label: string;
|
||||
/** Alternative text other than label to match by */
|
||||
alt?: string[];
|
||||
value: string;
|
||||
imgPath?: string;
|
||||
}
|
||||
|
||||
type ComboboxOption<T> = ComboboxBaseOption & T;
|
||||
interface ComboboxProps<T> {
|
||||
options: ComboboxOption<T>[];
|
||||
quickSelectOptions?: ComboboxOption<T>[];
|
||||
inputName: string;
|
||||
placeholder: string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
isLoading?: boolean;
|
||||
required?: boolean;
|
||||
value?: ComboboxOption<T> | null;
|
||||
initialValue: ComboboxOption<T> | null;
|
||||
onChange?: (selectedOption: ComboboxOption<T> | null) => void;
|
||||
fullWidth?: boolean;
|
||||
nullable?: true;
|
||||
fuseOptions?: IFuseOptions<ComboboxOption<T>>;
|
||||
}
|
||||
|
||||
export function Combobox<
|
||||
T extends Record<string, string | string[] | null | undefined | number>,
|
||||
>({
|
||||
options,
|
||||
quickSelectOptions,
|
||||
inputName,
|
||||
placeholder,
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
required,
|
||||
className,
|
||||
id,
|
||||
nullable,
|
||||
isLoading = false,
|
||||
fullWidth = false,
|
||||
fuseOptions = {},
|
||||
}: ComboboxProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const [_selectedOption, setSelectedOption] = React.useState<Unpacked<
|
||||
typeof options
|
||||
> | null>(initialValue);
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
const fuse = new Fuse(options, {
|
||||
...fuseOptions,
|
||||
keys: ["label", "alt"],
|
||||
});
|
||||
|
||||
const filteredOptions = (() => {
|
||||
if (!query) {
|
||||
if (quickSelectOptions) return quickSelectOptions;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
return fuse
|
||||
.search(query)
|
||||
.slice(0, MAX_RESULTS_SHOWN)
|
||||
.map((res) => res.item);
|
||||
})();
|
||||
|
||||
const noMatches = filteredOptions.length === 0;
|
||||
|
||||
const displayValue = (option: Unpacked<typeof options>) => {
|
||||
return option?.label ?? "";
|
||||
};
|
||||
|
||||
const selectedOption = value ?? _selectedOption;
|
||||
|
||||
const showComboboxOptions = () => {
|
||||
if (!quickSelectOptions || quickSelectOptions.length === 0) return;
|
||||
|
||||
buttonRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="combobox-wrapper">
|
||||
<HeadlessCombobox
|
||||
value={selectedOption}
|
||||
onChange={(selected) => {
|
||||
onChange?.(selected);
|
||||
setSelectedOption(selected);
|
||||
// https://github.com/tailwindlabs/headlessui/issues/1555
|
||||
// note that this still seems to be a problem despite what the issue says
|
||||
setTimeout(() => inputRef.current?.blur(), 0);
|
||||
}}
|
||||
name={inputName}
|
||||
disabled={!selectedOption && isLoading}
|
||||
// TODO: remove hack that prevents TS from freaking out. probably related: https://github.com/tailwindlabs/headlessui/issues/1895
|
||||
nullable={nullable as true}
|
||||
>
|
||||
<HeadlessCombobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={isLoading ? t("actions.loading") : placeholder}
|
||||
className={clsx("combobox-input", className, {
|
||||
fullWidth,
|
||||
})}
|
||||
defaultValue={initialValue}
|
||||
displayValue={displayValue}
|
||||
data-testid={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
required={required}
|
||||
autoComplete="off"
|
||||
onFocus={showComboboxOptions}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<HeadlessCombobox.Options
|
||||
className={clsx("combobox-options", {
|
||||
empty: noMatches,
|
||||
fullWidth,
|
||||
hidden:
|
||||
!query &&
|
||||
(!quickSelectOptions || quickSelectOptions.length === 0),
|
||||
})}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="combobox-no-matches">{t("actions.loading")}</div>
|
||||
) : noMatches ? (
|
||||
<div className="combobox-no-matches">
|
||||
{t("forms.errors.noSearchMatches")}{" "}
|
||||
<span className="combobox-emoji">🤔</span>
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((option) => (
|
||||
<HeadlessCombobox.Option
|
||||
key={option.value}
|
||||
value={option}
|
||||
as={React.Fragment}
|
||||
>
|
||||
{({ active }) => (
|
||||
<li className={clsx("combobox-item", { active })}>
|
||||
{option.imgPath && (
|
||||
<Image
|
||||
alt=""
|
||||
path={option.imgPath}
|
||||
width={24}
|
||||
height={24}
|
||||
className="combobox-item-image"
|
||||
/>
|
||||
)}
|
||||
<span className="combobox-item-label">{option.label}</span>
|
||||
</li>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))
|
||||
)}
|
||||
</HeadlessCombobox.Options>
|
||||
<HeadlessCombobox.Button ref={buttonRef} className="hidden" />
|
||||
</HeadlessCombobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WeaponCombobox({
|
||||
id,
|
||||
required,
|
||||
className,
|
||||
inputName,
|
||||
onChange,
|
||||
initialWeaponId,
|
||||
weaponIdsToOmit,
|
||||
fullWidth,
|
||||
nullable,
|
||||
value,
|
||||
quickSelectWeaponIds,
|
||||
}: Pick<
|
||||
ComboboxProps<ComboboxBaseOption>,
|
||||
| "inputName"
|
||||
| "onChange"
|
||||
| "className"
|
||||
| "id"
|
||||
| "required"
|
||||
| "fullWidth"
|
||||
| "nullable"
|
||||
> & {
|
||||
initialWeaponId?: (typeof mainWeaponIds)[number];
|
||||
weaponIdsToOmit?: Set<MainWeaponId>;
|
||||
value?: MainWeaponId | null;
|
||||
/** Weapons to show when there is focus but no query */
|
||||
quickSelectWeaponIds?: MainWeaponId[];
|
||||
}) {
|
||||
const { t, i18n } = useTranslation("weapons");
|
||||
|
||||
const alt = (id: (typeof mainWeaponIds)[number]) => {
|
||||
const result: string[] = [];
|
||||
|
||||
if (i18n.language !== "en") {
|
||||
result.push(t(`MAIN_${id}`, { lng: "en" }));
|
||||
}
|
||||
|
||||
const altNames = weaponAltNames.get(id);
|
||||
if (typeof altNames === "string") {
|
||||
result.push(altNames);
|
||||
} else if (Array.isArray(altNames)) {
|
||||
result.push(...altNames);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
const idToWeapon = (id: (typeof mainWeaponIds)[number]) => ({
|
||||
value: String(id),
|
||||
label: t(`MAIN_${id}`),
|
||||
imgPath: mainWeaponImageUrl(id),
|
||||
alt: alt(id),
|
||||
});
|
||||
|
||||
const options = mainWeaponIds
|
||||
.filter((id) => !weaponIdsToOmit?.has(id))
|
||||
.map(idToWeapon);
|
||||
|
||||
const quickSelectOptions = quickSelectWeaponIds?.flatMap((weaponId) => {
|
||||
return options.find((option) => option.value === String(weaponId)) ?? [];
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={options}
|
||||
quickSelectOptions={quickSelectOptions}
|
||||
value={typeof value === "number" ? idToWeapon(value) : null}
|
||||
initialValue={
|
||||
typeof initialWeaponId === "number" ? idToWeapon(initialWeaponId) : null
|
||||
}
|
||||
placeholder={t(`MAIN_${weaponCategories[0].weaponIds[0]}`)}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
id={id}
|
||||
required={required}
|
||||
fullWidth={fullWidth}
|
||||
nullable={nullable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AllWeaponCombobox({
|
||||
id,
|
||||
inputName,
|
||||
onChange,
|
||||
fullWidth,
|
||||
}: Pick<
|
||||
ComboboxProps<ComboboxBaseOption>,
|
||||
"inputName" | "onChange" | "id" | "fullWidth"
|
||||
>) {
|
||||
const { t } = useTranslation("weapons");
|
||||
|
||||
const options = () => {
|
||||
const result: ComboboxProps<
|
||||
Record<string, string | null | number>
|
||||
>["options"] = [];
|
||||
|
||||
for (const mainWeaponId of mainWeaponIds) {
|
||||
result.push({
|
||||
value: `MAIN_${mainWeaponId}`,
|
||||
label: t(`MAIN_${mainWeaponId}`),
|
||||
imgPath: mainWeaponImageUrl(mainWeaponId),
|
||||
});
|
||||
}
|
||||
|
||||
for (const subWeaponId of subWeaponIds) {
|
||||
if (nonBombSubWeaponIds.includes(subWeaponId)) continue;
|
||||
|
||||
result.push({
|
||||
value: `SUB_${subWeaponId}`,
|
||||
label: t(`SUB_${subWeaponId}`),
|
||||
imgPath: subWeaponImageUrl(subWeaponId),
|
||||
});
|
||||
}
|
||||
|
||||
for (const specialWeaponId of specialWeaponIds) {
|
||||
if (nonDamagingSpecialWeaponIds.includes(specialWeaponId)) continue;
|
||||
|
||||
result.push({
|
||||
value: `SPECIAL_${specialWeaponId}`,
|
||||
label: t(`SPECIAL_${specialWeaponId}`),
|
||||
imgPath: specialWeaponImageUrl(specialWeaponId),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={options()}
|
||||
initialValue={null}
|
||||
placeholder={t(`MAIN_${weaponCategories[0].weaponIds[0]}`)}
|
||||
onChange={onChange}
|
||||
id={id}
|
||||
fullWidth={fullWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GearCombobox({
|
||||
id,
|
||||
required,
|
||||
className,
|
||||
inputName,
|
||||
onChange,
|
||||
gearType,
|
||||
initialGearId,
|
||||
nullable,
|
||||
}: Pick<
|
||||
ComboboxProps<ComboboxBaseOption>,
|
||||
"inputName" | "onChange" | "className" | "id" | "required" | "nullable"
|
||||
> & { gearType: GearType; initialGearId?: number }) {
|
||||
const { t } = useTranslation("gear");
|
||||
|
||||
const translationPrefix =
|
||||
gearType === "HEAD" ? "H" : gearType === "CLOTHES" ? "C" : "S";
|
||||
const ids =
|
||||
gearType === "HEAD"
|
||||
? headGearIds
|
||||
: gearType === "CLOTHES"
|
||||
? clothesGearIds
|
||||
: shoesGearIds;
|
||||
|
||||
const idToGear = (id: (typeof ids)[number]) => ({
|
||||
value: String(id),
|
||||
label: t(`${translationPrefix}_${id}` as any),
|
||||
imgPath: gearImageUrl(gearType, id),
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={ids.map(idToGear)}
|
||||
placeholder={idToGear(ids[0]).label}
|
||||
initialValue={initialGearId ? idToGear(initialGearId as any) : null}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
id={id}
|
||||
required={required}
|
||||
nullable={nullable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapPoolEventToOption = (
|
||||
e: SerializedMapPoolEvent,
|
||||
): ComboboxOption<Pick<SerializedMapPoolEvent, "serializedMapPool">> => ({
|
||||
serializedMapPool: e.serializedMapPool,
|
||||
label: e.name,
|
||||
value: e.id.toString(),
|
||||
});
|
||||
|
||||
type MapPoolEventsComboboxProps = Pick<
|
||||
ComboboxProps<Pick<SerializedMapPoolEvent, "serializedMapPool">>,
|
||||
"inputName" | "className" | "id" | "required"
|
||||
> & {
|
||||
initialEvent?: SerializedMapPoolEvent;
|
||||
onChange: (event: SerializedMapPoolEvent | null) => void;
|
||||
};
|
||||
|
||||
export function MapPoolEventsCombobox({
|
||||
id,
|
||||
required,
|
||||
className,
|
||||
inputName,
|
||||
onChange,
|
||||
initialEvent,
|
||||
}: MapPoolEventsComboboxProps) {
|
||||
const { t } = useTranslation();
|
||||
const { events, isLoading, isError } = useAllEventsWithMapPools();
|
||||
|
||||
const options = React.useMemo(
|
||||
() => (events ? events.map(mapPoolEventToOption) : []),
|
||||
[events],
|
||||
);
|
||||
|
||||
// this is important so that we don't trigger the reset to the initialEvent every time
|
||||
const initialOption = React.useMemo(
|
||||
() => initialEvent && mapPoolEventToOption(initialEvent),
|
||||
[initialEvent],
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="text-sm text-error">{t("errors.genericReload")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={isLoading && initialOption ? [initialOption] : options}
|
||||
placeholder={t("actions.search")}
|
||||
initialValue={initialOption ?? null}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e && {
|
||||
id: Number.parseInt(e.value, 10),
|
||||
name: e.label,
|
||||
serializedMapPool: e.serializedMapPool,
|
||||
},
|
||||
);
|
||||
}}
|
||||
className={className}
|
||||
id={id}
|
||||
required={required}
|
||||
isLoading={isLoading}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
140
app/components/GearSelect.tsx
Normal file
140
app/components/GearSelect.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
import {
|
||||
SendouSelect,
|
||||
SendouSelectItem,
|
||||
SendouSelectItemSection,
|
||||
} from "~/components/elements/Select";
|
||||
import type { GearType } from "~/db/tables";
|
||||
import { brandIds } from "~/modules/in-game-lists/brand-ids";
|
||||
import {
|
||||
clothesGearBrandGrouped,
|
||||
headGearBrandGrouped,
|
||||
shoesGearBrandGrouped,
|
||||
} from "~/modules/in-game-lists/gear-ids";
|
||||
import type { BrandId } from "~/modules/in-game-lists/types";
|
||||
import { brandImageUrl, gearImageUrl } from "~/utils/urls";
|
||||
|
||||
import styles from "./WeaponSelect.module.css";
|
||||
|
||||
interface GearSelectProps<Clearable extends boolean | undefined = undefined> {
|
||||
label?: string;
|
||||
value?: number | (Clearable extends true ? null : never);
|
||||
initialValue?: number;
|
||||
onChange?: (
|
||||
weaponId: number | (Clearable extends true ? null : never),
|
||||
) => void;
|
||||
clearable?: Clearable;
|
||||
type: GearType;
|
||||
}
|
||||
|
||||
export function GearSelect<Clearable extends boolean | undefined = undefined>({
|
||||
label,
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
clearable,
|
||||
type,
|
||||
}: GearSelectProps<Clearable>) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const items = useGearItems(type);
|
||||
|
||||
return (
|
||||
<SendouSelect
|
||||
aria-label={!label ? t("common:forms.gearSearch.placeholder") : undefined}
|
||||
items={items}
|
||||
label={label}
|
||||
placeholder={t("common:forms.gearSearch.placeholder")}
|
||||
search={{
|
||||
placeholder: t("common:forms.gearSearch.search.placeholder"),
|
||||
}}
|
||||
className={styles.selectWidthWider}
|
||||
popoverClassName={styles.selectWidthWider}
|
||||
selectedKey={value}
|
||||
defaultSelectedKey={initialValue}
|
||||
onSelectionChange={(value) => onChange?.(value as any)}
|
||||
clearable={clearable}
|
||||
data-testid={`${type}-gear-select`}
|
||||
>
|
||||
{({ key, items: gear, brandId, idx }) => (
|
||||
<SendouSelectItemSection
|
||||
heading={
|
||||
<CategoryHeading
|
||||
brandId={brandId}
|
||||
className={idx === 0 ? "pt-0-5-forced" : undefined}
|
||||
/>
|
||||
}
|
||||
key={key}
|
||||
>
|
||||
{gear.map(({ id, name }) => (
|
||||
<SendouSelectItem key={id} id={id} textValue={name}>
|
||||
<div className={styles.item}>
|
||||
<Image
|
||||
path={gearImageUrl(type, id)}
|
||||
size={24}
|
||||
alt=""
|
||||
className={styles.weaponImg}
|
||||
/>
|
||||
<span
|
||||
className={styles.weaponLabel}
|
||||
data-testid={`gear-select-option-${name}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</SendouSelectItem>
|
||||
))}
|
||||
</SendouSelectItemSection>
|
||||
)}
|
||||
</SendouSelect>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryHeading({
|
||||
className,
|
||||
brandId,
|
||||
}: {
|
||||
className?: string;
|
||||
brandId: BrandId;
|
||||
}) {
|
||||
const { t } = useTranslation(["game-misc"]);
|
||||
|
||||
return (
|
||||
<div className={clsx(className, styles.categoryHeading)}>
|
||||
<Image path={brandImageUrl(brandId)} size={28} alt="" />
|
||||
{t(`game-misc:BRAND_${brandId}` as any)}
|
||||
<div className={styles.categoryDivider} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useGearItems(type: GearType) {
|
||||
const { t } = useTranslation(["gear", "game-misc"]);
|
||||
|
||||
const translationPrefix =
|
||||
type === "HEAD" ? "H" : type === "CLOTHES" ? "C" : "S";
|
||||
|
||||
const groupedGear =
|
||||
type === "HEAD"
|
||||
? headGearBrandGrouped
|
||||
: type === "CLOTHES"
|
||||
? clothesGearBrandGrouped
|
||||
: shoesGearBrandGrouped;
|
||||
|
||||
const items = brandIds.map((brandId, idx) => {
|
||||
const items = groupedGear[brandId] || [];
|
||||
|
||||
return {
|
||||
brandId,
|
||||
key: brandId,
|
||||
idx,
|
||||
items: items.map((gearId) => ({
|
||||
id: gearId,
|
||||
name: t(`${translationPrefix}_${gearId}` as any),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import * as React from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
|
|
@ -12,7 +11,6 @@ import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
|||
import { split, startsWith } from "~/utils/strings";
|
||||
import { assertType } from "~/utils/types";
|
||||
import { modeImageUrl, stageImageUrl } from "~/utils/urls";
|
||||
import { MapPoolEventsCombobox } from "./Combobox";
|
||||
import { SendouButton } from "./elements/Button";
|
||||
import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft";
|
||||
import { CrossIcon } from "./icons/Cross";
|
||||
|
|
@ -28,8 +26,6 @@ export type MapPoolSelectorProps = {
|
|||
event?: Pick<Tables["CalendarEvent"], "id" | "name">,
|
||||
) => void;
|
||||
className?: string;
|
||||
recentEvents?: SerializedMapPoolEvent[];
|
||||
initialEvent?: Pick<Tables["CalendarEvent"], "id" | "name">;
|
||||
title?: string;
|
||||
modesToInclude?: ModeShort[];
|
||||
info?: React.ReactNode;
|
||||
|
|
@ -45,8 +41,6 @@ export function MapPoolSelector({
|
|||
handleMapPoolChange,
|
||||
handleRemoval,
|
||||
className,
|
||||
recentEvents,
|
||||
initialEvent,
|
||||
title,
|
||||
modesToInclude,
|
||||
info,
|
||||
|
|
@ -57,15 +51,7 @@ export function MapPoolSelector({
|
|||
const { t } = useTranslation();
|
||||
|
||||
const [template, setTemplate] = React.useState<MapPoolTemplateValue>(
|
||||
initialEvent ? "event" : detectTemplate(mapPool),
|
||||
);
|
||||
|
||||
const [initialSerializedEvent, setInitialSerializedEvent] = React.useState(
|
||||
(): SerializedMapPoolEvent | undefined =>
|
||||
initialEvent && {
|
||||
...initialEvent,
|
||||
serializedMapPool: mapPool.serialized,
|
||||
},
|
||||
detectTemplate(mapPool),
|
||||
);
|
||||
|
||||
const handleStageModesChange = (newMapPool: MapPool) => {
|
||||
|
|
@ -85,14 +71,6 @@ export function MapPoolSelector({
|
|||
return;
|
||||
}
|
||||
|
||||
if (template === "event") {
|
||||
// If the user selected the "event" option, the _initial_ event passed via
|
||||
// props is likely not the current state and should not be prefilled
|
||||
// anymore.
|
||||
setInitialSerializedEvent(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWith(template, "preset:")) {
|
||||
const [, presetId] = split(template, ":");
|
||||
|
||||
|
|
@ -100,17 +78,6 @@ export function MapPoolSelector({
|
|||
return;
|
||||
}
|
||||
|
||||
if (startsWith(template, "recent-event:")) {
|
||||
const [, eventId] = split(template, ":");
|
||||
|
||||
const event = recentEvents?.find((e) => e.id.toString() === eventId);
|
||||
|
||||
if (event) {
|
||||
handleMapPoolChange(new MapPool(event.serializedMapPool), event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assertType<never, typeof template>();
|
||||
};
|
||||
|
||||
|
|
@ -141,14 +108,7 @@ export function MapPoolSelector({
|
|||
<MapPoolTemplateSelect
|
||||
value={template}
|
||||
handleChange={handleTemplateChange}
|
||||
recentEvents={recentEvents}
|
||||
/>
|
||||
{template === "event" && (
|
||||
<TemplateEventSelection
|
||||
initialEvent={initialSerializedEvent}
|
||||
handleEventChange={handleMapPoolChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{info}
|
||||
|
|
@ -355,11 +315,7 @@ type MapModePresetId = "ANARCHY" | "ALL" | ModeShort;
|
|||
|
||||
const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort];
|
||||
|
||||
type MapPoolTemplateValue =
|
||||
| "none"
|
||||
| `preset:${MapModePresetId}`
|
||||
| `recent-event:${string}`
|
||||
| "event";
|
||||
type MapPoolTemplateValue = "none" | `preset:${MapModePresetId}`;
|
||||
|
||||
function detectTemplate(mapPool: MapPool): MapPoolTemplateValue {
|
||||
for (const presetId of presetIds) {
|
||||
|
|
@ -393,7 +349,6 @@ function MapPoolTemplateSelect({
|
|||
}}
|
||||
>
|
||||
<option value="none">{t("common:maps.template.none")}</option>
|
||||
<option value="event">{t("common:maps.template.event")}</option>
|
||||
<optgroup label={t("common:maps.template.presets")}>
|
||||
{(["ANARCHY", "ALL"] as const).map((presetId) => (
|
||||
<option key={presetId} value={`preset:${presetId}`}>
|
||||
|
|
@ -421,37 +376,3 @@ function MapPoolTemplateSelect({
|
|||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
type TemplateEventSelectionProps = {
|
||||
handleEventChange: (
|
||||
mapPool: MapPool,
|
||||
event?: Pick<Tables["CalendarEvent"], "id" | "name">,
|
||||
) => void;
|
||||
initialEvent?: SerializedMapPoolEvent;
|
||||
};
|
||||
function TemplateEventSelection({
|
||||
handleEventChange,
|
||||
initialEvent,
|
||||
}: TemplateEventSelectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<label className="stack sm">
|
||||
{t("maps.template.event")}
|
||||
<MapPoolEventsCombobox
|
||||
id={id}
|
||||
inputName={id}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
handleEventChange(new MapPool(e.serializedMapPool), {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
});
|
||||
}
|
||||
}}
|
||||
initialEvent={initialEvent}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
48
app/components/WeaponSelect.module.css
Normal file
48
app/components/WeaponSelect.module.css
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
.selectWidthWider {
|
||||
--select-width: 250px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.weaponImg {
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.weaponLabel {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.categoryHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
font-weight: bold;
|
||||
color: var(--text-lighter);
|
||||
text-transform: uppercase;
|
||||
font-size: var(--fonts-xxs);
|
||||
padding-block-start: var(--s-2-5);
|
||||
padding-block-end: var(--s-1);
|
||||
padding-inline: var(--s-1-5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.categoryDivider {
|
||||
background-color: var(--border);
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
margin-block: var(--s-2);
|
||||
}
|
||||
|
||||
.categoryHeading img {
|
||||
border-radius: 100%;
|
||||
background-color: var(--bg-lightest);
|
||||
padding: var(--s-1);
|
||||
min-width: 28px;
|
||||
}
|
||||
280
app/components/WeaponSelect.tsx
Normal file
280
app/components/WeaponSelect.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
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 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,
|
||||
weaponCategories,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import {
|
||||
specialWeaponImageUrl,
|
||||
subWeaponImageUrl,
|
||||
weaponCategoryUrl,
|
||||
} from "~/utils/urls";
|
||||
|
||||
import styles from "./WeaponSelect.module.css";
|
||||
|
||||
interface WeaponSelectProps<
|
||||
Clearable extends boolean | undefined = undefined,
|
||||
IncludeSubSpecial extends boolean | undefined = undefined,
|
||||
> {
|
||||
label?: string;
|
||||
value?:
|
||||
| (IncludeSubSpecial extends true ? AnyWeapon : MainWeaponId)
|
||||
| (Clearable extends true ? null : never);
|
||||
initialValue?: IncludeSubSpecial extends true ? AnyWeapon : MainWeaponId;
|
||||
onChange?: (
|
||||
weaponId:
|
||||
| (IncludeSubSpecial extends true ? AnyWeapon : MainWeaponId)
|
||||
| (Clearable extends true ? null : never),
|
||||
) => void;
|
||||
clearable?: Clearable;
|
||||
includeSubSpecial?: IncludeSubSpecial;
|
||||
disabledWeaponIds?: Array<MainWeaponId>; // TODO: implement for `AnyWeapon` if needed
|
||||
testId?: string;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
// TODO: fix selected value disappears when filtered out. This is because `items` is filtered in a controlled manner and the selected key might not be included in the filtered items.
|
||||
export function WeaponSelect<
|
||||
Clearable extends boolean | undefined = undefined,
|
||||
IncludeSubSpecial extends boolean | undefined = undefined,
|
||||
>({
|
||||
label,
|
||||
value,
|
||||
initialValue,
|
||||
onChange,
|
||||
disabledWeaponIds,
|
||||
clearable,
|
||||
includeSubSpecial,
|
||||
testId = "weapon-select",
|
||||
isRequired,
|
||||
}: WeaponSelectProps<Clearable, IncludeSubSpecial>) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { items, filterValue, setFilterValue } =
|
||||
useFilteredWeaponItems(includeSubSpecial);
|
||||
|
||||
const keyify = (value?: MainWeaponId | AnyWeapon | null) => {
|
||||
if (typeof value === "number") return `MAIN_${value}`;
|
||||
if (!value) return;
|
||||
|
||||
return `${value.type}_${value.id}`;
|
||||
};
|
||||
|
||||
const handleOnChange = (key: Key | null) => {
|
||||
if (key === null) return onChange?.(null as any);
|
||||
const [type, id] = (key as string).split("_");
|
||||
const weapon = {
|
||||
id: Number(id),
|
||||
type: type as "MAIN" | "SUB" | "SPECIAL",
|
||||
} as AnyWeapon;
|
||||
|
||||
if (!includeSubSpecial) return onChange?.(weapon.id as any); // plain main weapon id
|
||||
|
||||
onChange?.(weapon as any);
|
||||
};
|
||||
|
||||
return (
|
||||
<SendouSelect
|
||||
aria-label={
|
||||
!label ? t("common:forms.weaponSearch.placeholder") : undefined
|
||||
}
|
||||
items={items}
|
||||
label={label}
|
||||
placeholder={t("common:forms.weaponSearch.placeholder")}
|
||||
search={{
|
||||
placeholder: t("common:forms.weaponSearch.search.placeholder"),
|
||||
}}
|
||||
className={styles.selectWidthWider}
|
||||
popoverClassName={styles.selectWidthWider}
|
||||
searchInputValue={filterValue}
|
||||
onSearchInputChange={setFilterValue}
|
||||
selectedKey={keyify(value)}
|
||||
defaultSelectedKey={keyify(initialValue)}
|
||||
onSelectionChange={handleOnChange}
|
||||
clearable={clearable}
|
||||
data-testid={testId}
|
||||
isRequired={isRequired}
|
||||
>
|
||||
{({ key, items: weapons, name, idx }) => (
|
||||
<SendouSelectItemSection
|
||||
heading={
|
||||
<CategoryHeading
|
||||
name={name}
|
||||
className={idx === 0 ? "pt-0-5-forced" : undefined}
|
||||
/>
|
||||
}
|
||||
key={key}
|
||||
>
|
||||
{weapons.map(({ weapon, name }) => (
|
||||
<SendouSelectItem
|
||||
key={weapon.anyWeaponId}
|
||||
id={weapon.anyWeaponId}
|
||||
textValue={name}
|
||||
isDisabled={
|
||||
includeSubSpecial
|
||||
? false
|
||||
: disabledWeaponIds?.includes(weapon.id as MainWeaponId)
|
||||
}
|
||||
>
|
||||
<div className={styles.item}>
|
||||
{weapon.type === "MAIN" ? (
|
||||
<WeaponImage
|
||||
weaponSplId={weapon.id}
|
||||
variant="build"
|
||||
size={24}
|
||||
className={styles.weaponImg}
|
||||
/>
|
||||
) : weapon.type === "SUB" ? (
|
||||
<Image
|
||||
path={subWeaponImageUrl(weapon.id)}
|
||||
size={24}
|
||||
alt=""
|
||||
className={styles.weaponImg}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
path={specialWeaponImageUrl(weapon.id)}
|
||||
size={24}
|
||||
alt=""
|
||||
className={styles.weaponImg}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={styles.weaponLabel}
|
||||
data-testid={`weapon-select-option-${name}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
</SendouSelectItem>
|
||||
))}
|
||||
</SendouSelectItemSection>
|
||||
)}
|
||||
</SendouSelect>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryHeading({
|
||||
name,
|
||||
className,
|
||||
}: {
|
||||
name: (typeof weaponCategories)[number]["name"] | "subs" | "specials";
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
const path = () => {
|
||||
if (name === "subs") return subWeaponImageUrl(SPLAT_BOMB_ID);
|
||||
if (name === "specials") return specialWeaponImageUrl(TRIZOOKA_ID);
|
||||
|
||||
return weaponCategoryUrl(name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(className, styles.categoryHeading)}>
|
||||
<Image path={path()} size={28} alt="" />
|
||||
{t(`common:weapon.category.${name}`)}
|
||||
<div className={styles.categoryDivider} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useFilteredWeaponItems(includeSubSpecial: boolean | undefined) {
|
||||
const items = useAllWeaponCategories(includeSubSpecial);
|
||||
const [filterValue, setFilterValue] = React.useState("");
|
||||
|
||||
const filtered = !filterValue
|
||||
? items
|
||||
: items
|
||||
.map((category) => {
|
||||
const filteredItems = category.items.filter((item) =>
|
||||
filterWeapon({
|
||||
weapon: item.weapon,
|
||||
weaponName: item.name,
|
||||
searchTerm: filterValue,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...category,
|
||||
items: filteredItems,
|
||||
};
|
||||
})
|
||||
.filter((category) => category.items.length > 0)
|
||||
.map((category, idx) => ({ ...category, idx }));
|
||||
|
||||
return {
|
||||
items: filtered,
|
||||
filterValue,
|
||||
setFilterValue,
|
||||
};
|
||||
}
|
||||
|
||||
function useAllWeaponCategories(withSubSpecial = false) {
|
||||
const { t } = useTranslation(["weapons"]);
|
||||
|
||||
const mainWeaponCategories = weaponCategories.map((category, idx) => ({
|
||||
name: category.name,
|
||||
key: category.name,
|
||||
idx,
|
||||
items: category.weaponIds.map((id) => ({
|
||||
name: t(`weapons:MAIN_${id}`),
|
||||
weapon: {
|
||||
anyWeaponId: `MAIN_${id}`,
|
||||
id,
|
||||
type: "MAIN" as const,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
if (!withSubSpecial) {
|
||||
return mainWeaponCategories;
|
||||
}
|
||||
|
||||
const subWeaponCategory = {
|
||||
name: "subs" as const,
|
||||
key: "subs",
|
||||
idx: 0,
|
||||
items: subWeaponIds.map((id) => ({
|
||||
name: t(`weapons:SUB_${id}`),
|
||||
weapon: {
|
||||
anyWeaponId: `SUB_${id}`,
|
||||
id,
|
||||
type: "SUB" as const,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
const specialWeaponCategory = {
|
||||
name: "specials" as const,
|
||||
key: "specials",
|
||||
idx: 1,
|
||||
items: specialWeaponIds.map((id) => ({
|
||||
name: t(`weapons:SPECIAL_${id}`),
|
||||
weapon: {
|
||||
anyWeaponId: `SPECIAL_${id}`,
|
||||
id,
|
||||
type: "SPECIAL" as const,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return [
|
||||
subWeaponCategory,
|
||||
specialWeaponCategory,
|
||||
...mainWeaponCategories.map((c) => ({ ...c, idx: c.idx + 2 })),
|
||||
];
|
||||
}
|
||||
|
|
@ -78,6 +78,11 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item[data-disabled] {
|
||||
color: var(--text-lighter);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.itemFocused {
|
||||
background-color: var(--theme-transparent);
|
||||
color: var(--text);
|
||||
|
|
@ -134,3 +139,13 @@
|
|||
padding-block: var(--s-8);
|
||||
color: var(--text-lighter);
|
||||
}
|
||||
|
||||
.select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
position: absolute;
|
||||
bottom: -17px;
|
||||
right: 9px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,33 @@
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import type { ListBoxItemProps, SelectProps } from "react-aria-components";
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
Header,
|
||||
Input,
|
||||
Label,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
ListBoxSection,
|
||||
ListLayout,
|
||||
Popover,
|
||||
SearchField,
|
||||
Select,
|
||||
SelectStateContext,
|
||||
SelectValue,
|
||||
Virtualizer,
|
||||
useFilter,
|
||||
} from "react-aria-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SendouBottomTexts } from "~/components/elements/BottomTexts";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { ChevronUpDownIcon } from "~/components/icons/ChevronUpDown";
|
||||
import { CrossIcon } from "../icons/Cross";
|
||||
import { SearchIcon } from "../icons/Search";
|
||||
import styles from "./Select.module.css";
|
||||
|
||||
interface SendouSelectProps<T extends object>
|
||||
export interface SendouSelectProps<T extends object>
|
||||
extends Omit<SelectProps<T>, "children"> {
|
||||
label?: string;
|
||||
description?: string;
|
||||
|
|
@ -33,8 +38,28 @@ interface SendouSelectProps<T extends object>
|
|||
search?: {
|
||||
placeholder?: string;
|
||||
};
|
||||
popoverClassName?: string;
|
||||
/** Value of the search input, used for controlled components */
|
||||
searchInputValue?: string;
|
||||
/** Callback for when the search input value changes. When defined `items` has to be filtered on the caller side (automatic filtering in component disabled). */
|
||||
onSearchInputChange?: (value: string) => void;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A customizable select component with optional search functionality. Virtualizes the list of items for performance.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <SendouSelect items={items} search={{ placeholder: "Search for items..." }}>
|
||||
* {({ key, ...item }) => (
|
||||
* <SendouSelectItem key={key} {...item}>
|
||||
* {item.name}
|
||||
* </SendouSelectItem>
|
||||
* )}
|
||||
* </SendouSelect>
|
||||
* ```
|
||||
*/
|
||||
export function SendouSelect<T extends object>({
|
||||
label,
|
||||
description,
|
||||
|
|
@ -43,13 +68,32 @@ export function SendouSelect<T extends object>({
|
|||
children,
|
||||
items,
|
||||
search,
|
||||
popoverClassName,
|
||||
searchInputValue,
|
||||
onSearchInputChange,
|
||||
clearable = false,
|
||||
className,
|
||||
...props
|
||||
}: SendouSelectProps<T>) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { contains } = useFilter({ sensitivity: "base" });
|
||||
|
||||
const isControlled = !!onSearchInputChange;
|
||||
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
if (!isControlled) return;
|
||||
|
||||
if (!isOpen) {
|
||||
onSearchInputChange("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<Select
|
||||
{...props}
|
||||
className={clsx(className, styles.select)}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
{label ? <Label>{label}</Label> : null}
|
||||
<Button className={styles.button}>
|
||||
<SelectValue className={styles.selectValue} />
|
||||
|
|
@ -57,9 +101,14 @@ export function SendouSelect<T extends object>({
|
|||
<ChevronUpDownIcon className={styles.icon} />
|
||||
</span>
|
||||
</Button>
|
||||
{clearable ? <SelectClearButton /> : null}
|
||||
<SendouBottomTexts bottomText={bottomText} errorText={errorText} />
|
||||
<Popover className={styles.popover}>
|
||||
<Autocomplete filter={contains}>
|
||||
<Popover className={clsx(popoverClassName, styles.popover)}>
|
||||
<Autocomplete
|
||||
filter={isControlled ? undefined : contains}
|
||||
inputValue={searchInputValue}
|
||||
onInputChange={onSearchInputChange}
|
||||
>
|
||||
{search ? (
|
||||
<SearchField
|
||||
aria-label="Search"
|
||||
|
|
@ -108,3 +157,40 @@ export function SendouSelectItem(props: SendouSelectItemProps) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface SendouSelectItemSectionProps {
|
||||
heading: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SendouSelectItemSection({
|
||||
heading,
|
||||
children,
|
||||
}: SendouSelectItemSectionProps) {
|
||||
return (
|
||||
<ListBoxSection>
|
||||
<Header>{heading}</Header>
|
||||
{children}
|
||||
</ListBoxSection>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectClearButton() {
|
||||
const state = React.useContext(SelectStateContext);
|
||||
|
||||
if (!state?.selectedKey) return null;
|
||||
|
||||
return (
|
||||
<SendouButton
|
||||
// Don't inherit behavior from Select.
|
||||
slot={null}
|
||||
variant="minimal-destructive"
|
||||
size="miniscule"
|
||||
icon={<CrossIcon />}
|
||||
onPress={() => state?.setSelectedKey(null)}
|
||||
className={styles.clearButton}
|
||||
>
|
||||
Clear
|
||||
</SendouButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export const UserSearch = React.forwardRef(function UserSearch<
|
|||
placeholder=""
|
||||
selectedKey={selectedKey}
|
||||
onSelectionChange={onSelectionChange as (key: Key | null) => void}
|
||||
aria-label="User search"
|
||||
{...rest}
|
||||
>
|
||||
{label ? (
|
||||
|
|
|
|||
34
app/features/art/components/TagSelect.tsx
Normal file
34
app/features/art/components/TagSelect.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
SendouSelect,
|
||||
SendouSelectItem,
|
||||
type SendouSelectProps,
|
||||
} from "~/components/elements/Select";
|
||||
|
||||
interface TagSelectProps<T extends object>
|
||||
extends Omit<SendouSelectProps<T>, "items" | "children"> {
|
||||
tags: Array<{ id: number; name: string }>;
|
||||
}
|
||||
|
||||
export function TagSelect<T extends object>({
|
||||
tags,
|
||||
...rest
|
||||
}: TagSelectProps<T>) {
|
||||
const { t } = useTranslation(["art"]);
|
||||
|
||||
return (
|
||||
<SendouSelect
|
||||
items={tags}
|
||||
placeholder={t("art:forms.tags.placeholder")}
|
||||
search={{ placeholder: t("art:forms.tags.search.placeholder") }}
|
||||
aria-label={t("art:forms.tags.placeholder")}
|
||||
{...rest}
|
||||
>
|
||||
{({ id, name, ...item }) => (
|
||||
<SendouSelectItem key={id} id={name} {...item}>
|
||||
{name}
|
||||
</SendouSelectItem>
|
||||
)}
|
||||
</SendouSelect>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import * as React from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useFetcher } from "react-router-dom";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { Combobox } from "~/components/Combobox";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
|
|
@ -25,6 +24,7 @@ import {
|
|||
import { metaTitle } from "../../../utils/remix";
|
||||
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";
|
||||
|
|
@ -205,11 +205,6 @@ function Tags() {
|
|||
);
|
||||
const [newTagValue, setNewTagValue] = React.useState("");
|
||||
|
||||
const existingTags = data.tags;
|
||||
const unselectedTags = existingTags.filter(
|
||||
(t) => !tags.some((tag) => tag.id === t.id),
|
||||
);
|
||||
|
||||
const handleAddNewTag = () => {
|
||||
const normalizedNewTagValue = newTagValue
|
||||
.trim()
|
||||
|
|
@ -224,7 +219,7 @@ function Tags() {
|
|||
return;
|
||||
}
|
||||
|
||||
const alreadyCreatedTag = existingTags.find(
|
||||
const alreadyCreatedTag = data.tags.find(
|
||||
(t) => t.name === normalizedNewTagValue,
|
||||
);
|
||||
|
||||
|
|
@ -287,23 +282,14 @@ function Tags() {
|
|||
</SendouButton>
|
||||
</div>
|
||||
) : (
|
||||
<Combobox
|
||||
<TagSelect
|
||||
// empty combobox on select
|
||||
key={tags.length}
|
||||
options={unselectedTags.map((t) => ({
|
||||
label: t.name,
|
||||
value: String(t.id),
|
||||
}))}
|
||||
inputName="tags"
|
||||
placeholder={t("art:forms.tags.searchExisting.placeholder")}
|
||||
initialValue={null}
|
||||
onChange={(selection) => {
|
||||
if (!selection) return;
|
||||
setTags([
|
||||
...tags,
|
||||
{ name: selection.label, id: Number(selection.value) },
|
||||
]);
|
||||
}}
|
||||
tags={data.tags}
|
||||
disabledKeys={tags.map((t) => t.id).filter((id) => id !== undefined)}
|
||||
onSelectionChange={(tagName) =>
|
||||
setTags([...tags, data.tags.find((t) => t.name === tagName)!])
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="text-sm stack sm flex-wrap horizontal">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { ShouldRevalidateFunction } from "@remix-run/react";
|
|||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddNewButton } from "~/components/AddNewButton";
|
||||
import { Combobox } from "~/components/Combobox";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
|
|
@ -14,6 +13,7 @@ import { artPage, navIconUrl, newArtPage } from "~/utils/urls";
|
|||
import { metaTags } from "../../../utils/remix";
|
||||
import { FILTERED_TAG_KEY_SEARCH_PARAM_KEY } from "../art-constants";
|
||||
import { ArtGrid } from "../components/ArtGrid";
|
||||
import { TagSelect } from "../components/TagSelect";
|
||||
|
||||
import { loader } from "../loaders/art.server";
|
||||
export { loader };
|
||||
|
|
@ -87,20 +87,12 @@ export default function ArtPage() {
|
|||
</Label>
|
||||
</div>
|
||||
<div className="stack horizontal sm items-center">
|
||||
<Combobox
|
||||
<TagSelect
|
||||
key={filteredTag}
|
||||
options={data.allTags.map((t) => ({
|
||||
label: t.name,
|
||||
value: String(t.id),
|
||||
}))}
|
||||
inputName="tags"
|
||||
placeholder={t("art:filterByTag")}
|
||||
initialValue={null}
|
||||
onChange={(selection) => {
|
||||
if (!selection) return;
|
||||
|
||||
tags={data.allTags}
|
||||
onSelectionChange={(tagName) => {
|
||||
setSearchParams((prev) => {
|
||||
prev.set(FILTERED_TAG_KEY_SEARCH_PARAM_KEY, selection.label);
|
||||
prev.set(FILTERED_TAG_KEY_SEARCH_PARAM_KEY, tagName as string);
|
||||
return prev;
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useTranslation } from "react-i18next";
|
|||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import Chart from "~/components/Chart";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { Image } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Table } from "~/components/Table";
|
||||
|
|
@ -86,6 +85,7 @@ import {
|
|||
} from "../core/utils";
|
||||
import "../analyzer.css";
|
||||
import * as R from "remeda";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
|
||||
export const CURRENT_PATCH = "10.0";
|
||||
|
|
@ -246,15 +246,14 @@ function BuildAnalyzerPage() {
|
|||
<div className="analyzer__left-column">
|
||||
<div className="stack sm items-center w-full">
|
||||
<div className="w-full">
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
onChange={(opt) =>
|
||||
opt &&
|
||||
<WeaponSelect
|
||||
label={t("analyzer:weaponSelect.label")}
|
||||
initialValue={mainWeaponId}
|
||||
onChange={(val) =>
|
||||
handleChange({
|
||||
newMainWeaponId: Number(opt.value) as MainWeaponId,
|
||||
newMainWeaponId: val,
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import type {
|
|||
TournamentSettings,
|
||||
} from "~/db/tables";
|
||||
import { EXCLUDED_TAGS } from "~/features/calendar/calendar-constants";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
import {
|
||||
databaseTimestampNow,
|
||||
|
|
@ -429,29 +428,6 @@ function tagsArray(args: {
|
|||
return tags;
|
||||
}
|
||||
|
||||
export async function findRecentMapPoolsByAuthorId(authorId: number) {
|
||||
const rows = await db
|
||||
.selectFrom("CalendarEvent")
|
||||
.innerJoin("MapPoolMap", "CalendarEvent.id", "MapPoolMap.calendarEventId")
|
||||
.select(({ eb }) => [
|
||||
"CalendarEvent.id",
|
||||
"CalendarEvent.name",
|
||||
withMapPool(eb),
|
||||
])
|
||||
.where("CalendarEvent.authorId", "=", authorId)
|
||||
.where("CalendarEvent.hidden", "=", 0)
|
||||
.orderBy("CalendarEvent.id", "desc")
|
||||
.groupBy("CalendarEvent.id")
|
||||
.limit(5)
|
||||
.execute();
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
serializedMapPool: MapPool.serialize(row.mapPool),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function findResultsByEventId(eventId: number) {
|
||||
return db
|
||||
.selectFrom("CalendarEventResultTeam")
|
||||
|
|
@ -483,31 +459,6 @@ export async function findResultsByEventId(eventId: number) {
|
|||
.execute();
|
||||
}
|
||||
|
||||
export async function allEventsWithMapPools() {
|
||||
const rows = await db
|
||||
.selectFrom("CalendarEvent")
|
||||
.select(({ eb }) => [
|
||||
"CalendarEvent.id",
|
||||
"CalendarEvent.name",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("MapPoolMap")
|
||||
.select(["MapPoolMap.stageId", "MapPoolMap.mode"])
|
||||
.whereRef("MapPoolMap.calendarEventId", "=", "CalendarEvent.id"),
|
||||
).as("mapPool"),
|
||||
])
|
||||
.innerJoin("MapPoolMap", "CalendarEvent.id", "MapPoolMap.calendarEventId")
|
||||
.groupBy("CalendarEvent.id")
|
||||
.orderBy("CalendarEvent.id", "desc")
|
||||
.execute();
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
serializedMapPool: MapPool.serialize(row.mapPool),
|
||||
}));
|
||||
}
|
||||
|
||||
type CreateArgs = Pick<
|
||||
Tables["CalendarEvent"],
|
||||
| "name"
|
||||
|
|
|
|||
|
|
@ -70,8 +70,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
eventToEdit?.tournament,
|
||||
),
|
||||
managedBadges: await BadgeRepository.findManagedByUserId(user.id),
|
||||
recentEventsWithMapPools:
|
||||
await CalendarRepository.findRecentMapPoolsByAuthorId(user.id),
|
||||
eventToEdit: canEditEvent ? eventToEdit : undefined,
|
||||
eventToCopy:
|
||||
user.roles.includes("TOURNAMENT_ADDER") && !eventToEdit
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import {
|
|||
calendarEditPage,
|
||||
calendarEventPage,
|
||||
calendarReportWinnersPage,
|
||||
mapsPageWithMapPool,
|
||||
navIconUrl,
|
||||
readonlyMapsPage,
|
||||
resolveBaseUrl,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
|
|
@ -271,13 +271,15 @@ function MapPoolInfo() {
|
|||
|
||||
if (!data.event.mapPool || data.event.mapPool.length === 0) return null;
|
||||
|
||||
const mapPool = new MapPool(data.event.mapPool);
|
||||
|
||||
return (
|
||||
<Section title={t("calendar:forms.mapPool")}>
|
||||
<div className="event__map-pool-section">
|
||||
<MapPoolStages mapPool={new MapPool(data.event.mapPool)} />
|
||||
<MapPoolStages mapPool={mapPool} />
|
||||
<LinkButton
|
||||
className="event__create-map-list-link"
|
||||
to={readonlyMapsPage(data.event.eventId)}
|
||||
to={mapsPageWithMapPool(mapPool)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1073,8 +1073,7 @@ const mapPickingStyleToShort: Record<
|
|||
function TournamentMapPickingStyleSelect() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const id = React.useId();
|
||||
const { eventToEdit, recentEventsWithMapPools } =
|
||||
useLoaderData<typeof loader>();
|
||||
const { eventToEdit } = useLoaderData<typeof loader>();
|
||||
const baseEvent = useBaseEvent();
|
||||
const [mode, setMode] = React.useState<"ALL" | "TO" | RankedModeShort>(
|
||||
baseEvent?.mapPickingStyle
|
||||
|
|
@ -1123,7 +1122,6 @@ function TournamentMapPickingStyleSelect() {
|
|||
mapPool={mapPool}
|
||||
title={t("common:maps.mapPool")}
|
||||
handleMapPoolChange={setMapPool}
|
||||
recentEvents={recentEventsWithMapPools}
|
||||
allowBulkEdit
|
||||
/>
|
||||
</>
|
||||
|
|
@ -1136,7 +1134,6 @@ function MapPoolSection() {
|
|||
const { t } = useTranslation(["game-misc", "common"]);
|
||||
|
||||
const baseEvent = useBaseEvent();
|
||||
const { recentEventsWithMapPools } = useLoaderData<typeof loader>();
|
||||
const [mapPool, setMapPool] = React.useState<MapPool>(
|
||||
baseEvent?.mapPool ? new MapPool(baseEvent.mapPool) : MapPool.EMPTY,
|
||||
);
|
||||
|
|
@ -1156,7 +1153,6 @@ function MapPoolSection() {
|
|||
title={t("common:maps.mapPool")}
|
||||
handleRemoval={() => setIncludeMapPool(false)}
|
||||
handleMapPoolChange={setMapPool}
|
||||
recentEvents={recentEventsWithMapPools}
|
||||
allowBulkEdit
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import type { SerializeFrom } from "@remix-run/node";
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
|
||||
export const loader = async () => {
|
||||
return {
|
||||
events: await CalendarRepository.allEventsWithMapPools(),
|
||||
};
|
||||
};
|
||||
|
||||
export type EventsWithMapPoolsLoaderData = SerializeFrom<typeof loader>;
|
||||
export type SerializedMapPoolEvent =
|
||||
EventsWithMapPoolsLoaderData["events"][number];
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import * as R from "remeda";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import type { Tables } from "~/db/tables";
|
||||
|
|
@ -123,20 +123,18 @@ function WeaponFilterFields({
|
|||
}) {
|
||||
return (
|
||||
<div className="stack horizontal sm flex-wrap">
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
key={value.length}
|
||||
weaponIdsToOmit={new Set(value)}
|
||||
onChange={(wpn) =>
|
||||
wpn &&
|
||||
<WeaponSelect
|
||||
disabledWeaponIds={value}
|
||||
onChange={(weaponId) =>
|
||||
changeFilter({
|
||||
_tag: "Weapon",
|
||||
weaponSplIds:
|
||||
value.length >= 10
|
||||
? [...value.slice(1, 10), Number(wpn.value) as MainWeaponId]
|
||||
: [...value, Number(wpn.value) as MainWeaponId],
|
||||
? [...value.slice(1, 10), weaponId]
|
||||
: [...value, weaponId],
|
||||
})
|
||||
}
|
||||
key={value.join("-")}
|
||||
/>
|
||||
{value.map((weapon) => (
|
||||
<SendouButton
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await getUserId(request);
|
||||
const url = new URL(request.url);
|
||||
const calendarEventId = url.searchParams.get("eventId");
|
||||
|
||||
const event = calendarEventId
|
||||
? await CalendarRepository.findById({
|
||||
id: Number(calendarEventId),
|
||||
includeMapPool: true,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
calendarEvent: event
|
||||
? {
|
||||
id: event.eventId,
|
||||
name: event.name,
|
||||
mapPool: event.mapPool,
|
||||
}
|
||||
: undefined,
|
||||
recentEventsWithMapPools: user
|
||||
? await CalendarRepository.findRecentMapPoolsByAuthorId(user.id)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Link, useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import { useSearchParams } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
|
@ -9,19 +9,13 @@ import { Main } from "~/components/Main";
|
|||
import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { EditIcon } from "~/components/icons/Edit";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { ModeWithStage } from "~/modules/in-game-lists/types";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
MAPS_URL,
|
||||
calendarEventPage,
|
||||
ipLabsMaps,
|
||||
navIconUrl,
|
||||
} from "~/utils/urls";
|
||||
import { MAPS_URL, ipLabsMaps, navIconUrl } from "~/utils/urls";
|
||||
import { generateMapList } from "../core/map-list-generator/map-list";
|
||||
import { modesOrder } from "../core/map-list-generator/modes";
|
||||
import { mapPoolToNonEmptyModes } from "../core/map-list-generator/utils";
|
||||
|
|
@ -29,9 +23,6 @@ import { MapPool } from "../core/map-pool";
|
|||
|
||||
import styles from "./maps.module.css";
|
||||
|
||||
import { loader } from "../loaders/maps.server";
|
||||
export { loader };
|
||||
|
||||
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
|
||||
|
||||
export const shouldRevalidate: ShouldRevalidateFunction = ({ nextUrl }) => {
|
||||
|
|
@ -62,39 +53,17 @@ export const handle: SendouRouteHandle = {
|
|||
|
||||
export default function MapListPage() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { mapPool, handleMapPoolChange, readonly, switchToEditMode } =
|
||||
const { mapPool, handleMapPoolChange, readonly } =
|
||||
useSearchParamPersistedMapPool();
|
||||
|
||||
return (
|
||||
<Main className={`${styles.container} stack lg`}>
|
||||
{searchParams.has("readonly") && data.calendarEvent && (
|
||||
<div className={styles.poolMeta}>
|
||||
<div className={styles.poolInfo}>
|
||||
{t("common:maps.mapPool")}:{" "}
|
||||
<Link to={calendarEventPage(data.calendarEvent.id)}>
|
||||
{data.calendarEvent.name}
|
||||
</Link>
|
||||
</div>
|
||||
<SendouButton
|
||||
variant="outlined"
|
||||
onPress={switchToEditMode}
|
||||
size="small"
|
||||
icon={<EditIcon />}
|
||||
>
|
||||
{t("common:actions.edit")}
|
||||
</SendouButton>
|
||||
</div>
|
||||
)}
|
||||
{readonly ? (
|
||||
<MapPoolStages mapPool={mapPool} />
|
||||
) : (
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
recentEvents={data.recentEventsWithMapPools}
|
||||
initialEvent={data.calendarEvent}
|
||||
allowBulkEdit
|
||||
className={styles.poolSelector}
|
||||
/>
|
||||
|
|
@ -113,7 +82,6 @@ export default function MapListPage() {
|
|||
}
|
||||
|
||||
export function useSearchParamPersistedMapPool() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [mapPool, setMapPool] = React.useState(() => {
|
||||
|
|
@ -121,10 +89,6 @@ export function useSearchParamPersistedMapPool() {
|
|||
return new MapPool(searchParams.get("pool")!);
|
||||
}
|
||||
|
||||
if (data.calendarEvent?.mapPool) {
|
||||
return new MapPool(data.calendarEvent.mapPool);
|
||||
}
|
||||
|
||||
return MapPool.ANARCHY;
|
||||
});
|
||||
|
||||
|
|
@ -143,20 +107,10 @@ export function useSearchParamPersistedMapPool() {
|
|||
);
|
||||
};
|
||||
|
||||
const switchToEditMode = () => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete("readonly");
|
||||
setSearchParams(newSearchParams, {
|
||||
replace: false,
|
||||
preventScrollReset: true,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
mapPool,
|
||||
readonly: searchParams.has("readonly"),
|
||||
handleMapPoolChange,
|
||||
switchToEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import clsx from "clsx";
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { AllWeaponCombobox } from "~/components/Combobox";
|
||||
import { Image, WeaponImage } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import type { AnyWeapon, DamageType } from "~/features/build-analyzer";
|
||||
import type { DamageType } from "~/features/build-analyzer";
|
||||
import { possibleApValues } from "~/features/build-analyzer";
|
||||
import {
|
||||
BIG_BUBBLER_ID,
|
||||
|
|
@ -39,6 +38,7 @@ import { useObjectDamage } from "../calculator-hooks";
|
|||
import type { DamageReceiver } from "../calculator-types";
|
||||
import "../calculator.css";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
|
|
@ -84,22 +84,13 @@ export default function ObjectDamagePage() {
|
|||
<div className="object-damage__selects">
|
||||
<div className="object-damage__selects__weapon">
|
||||
<Label htmlFor="weapon">{t("analyzer:labels.weapon")}</Label>
|
||||
<AllWeaponCombobox
|
||||
id="weapon"
|
||||
inputName="weapon"
|
||||
onChange={(opt) => {
|
||||
if (!opt) return;
|
||||
|
||||
const [type, id] = opt.value.split("_");
|
||||
|
||||
<WeaponSelect
|
||||
includeSubSpecial
|
||||
onChange={(newAnyWeapon) => {
|
||||
handleChange({
|
||||
newAnyWeapon: {
|
||||
id: Number(id),
|
||||
type,
|
||||
} as AnyWeapon,
|
||||
newAnyWeapon,
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
{allDamageTypes.length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import { Flipped, Flipper } from "react-flip-toolkit";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { Divider } from "~/components/Divider";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Image, ModeImage, StageImage, WeaponImage } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { LinkButton } from "~/components/elements/Button";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouPopover } from "~/components/elements/Popover";
|
||||
|
|
@ -30,7 +30,6 @@ import {
|
|||
SendouTabs,
|
||||
} from "~/components/elements/Tabs";
|
||||
import { ArchiveBoxIcon } from "~/components/icons/ArchiveBox";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { RefreshArrowsIcon } from "~/components/icons/RefreshArrows";
|
||||
import { ScaleIcon } from "~/components/icons/Scale";
|
||||
|
|
@ -42,7 +41,6 @@ import { AddPrivateNoteDialog } from "~/features/sendouq-match/components/AddPri
|
|||
import type { ReportedWeaponForMerging } from "~/features/sendouq-match/core/reported-weapons.server";
|
||||
import { GroupCard } from "~/features/sendouq/components/GroupCard";
|
||||
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
|
||||
import { useRecentlyReportedWeapons } from "~/features/sendouq/q-hooks";
|
||||
import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useWindowSize } from "~/hooks/useWindowSize";
|
||||
|
|
@ -414,8 +412,6 @@ function ReportWeaponsForm() {
|
|||
const [reportingMode, setReportingMode] = React.useState<
|
||||
"ALL" | "MYSELF" | "MY_TEAM"
|
||||
>("MYSELF");
|
||||
const { recentlyReportedWeapons, addRecentlyReportedWeapon } =
|
||||
useRecentlyReportedWeapons();
|
||||
|
||||
const playedMaps = data.match.mapList.filter((m) => m.winnerGroupId);
|
||||
const winners = playedMaps.map((m) =>
|
||||
|
|
@ -479,7 +475,7 @@ function ReportWeaponsForm() {
|
|||
name="weapons"
|
||||
value={JSON.stringify(weaponsUsage)}
|
||||
/>
|
||||
<div className="stack horizontal sm justify-between w-max mx-auto">
|
||||
<div className="stack horizontal md justify-between w-max mx-auto">
|
||||
<h3 className="text-md">{t("q:match.report.whoToReport")}</h3>
|
||||
<label className="stack horizontal xs items-center mb-0">
|
||||
{t("q:match.report.whoToReport.me")}
|
||||
|
|
@ -573,27 +569,9 @@ function ReportWeaponsForm() {
|
|||
)}
|
||||
</div>
|
||||
<div className="stack horizontal sm items-center">
|
||||
<WeaponImage
|
||||
weaponSplId={weaponSplId ?? 0}
|
||||
variant="badge"
|
||||
width={32}
|
||||
className={clsx("ml-auto", {
|
||||
invisible: typeof weaponSplId !== "number",
|
||||
})}
|
||||
/>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
value={weaponSplId}
|
||||
quickSelectWeaponIds={recentlyReportedWeapons}
|
||||
onChange={(weapon) => {
|
||||
if (!weapon) return;
|
||||
|
||||
const weaponSplId = Number(
|
||||
weapon.value,
|
||||
) as MainWeaponId;
|
||||
|
||||
addRecentlyReportedWeapon(weaponSplId);
|
||||
|
||||
<WeaponSelect
|
||||
value={weaponSplId ?? undefined}
|
||||
onChange={(weaponSplId) => {
|
||||
setWeaponsUsage((val) => {
|
||||
const result = val.filter(
|
||||
(reportedWeapon) =>
|
||||
|
|
@ -953,8 +931,6 @@ function MapList({
|
|||
const [ownWeaponsUsage, setOwnWeaponsUsage] = React.useState<
|
||||
ReportedWeaponForMerging[]
|
||||
>([]);
|
||||
const { recentlyReportedWeapons, addRecentlyReportedWeapon } =
|
||||
useRecentlyReportedWeapons();
|
||||
|
||||
const previouslyReportedWinners = isResubmission
|
||||
? data.match.mapList
|
||||
|
|
@ -1000,11 +976,6 @@ function MapList({
|
|||
setWinners={setWinners}
|
||||
weapons={data.reportedWeapons?.[i]}
|
||||
showReportedOwnWeapon={!ownWeaponReported}
|
||||
recentlyReportedWeapons={recentlyReportedWeapons}
|
||||
addRecentlyReportedWeapon={addRecentlyReportedWeapon}
|
||||
ownWeapon={
|
||||
ownWeaponsUsage.find((w) => w.mapIndex === i)?.weaponSplId
|
||||
}
|
||||
onOwnWeaponSelected={(newReportedWeapon) => {
|
||||
if (!newReportedWeapon) return;
|
||||
|
||||
|
|
@ -1060,11 +1031,8 @@ function MapListMap({
|
|||
setWinners,
|
||||
canReportScore,
|
||||
weapons,
|
||||
ownWeapon,
|
||||
onOwnWeaponSelected,
|
||||
showReportedOwnWeapon,
|
||||
recentlyReportedWeapons,
|
||||
addRecentlyReportedWeapon,
|
||||
}: {
|
||||
i: number;
|
||||
map: Unpacked<SerializeFrom<typeof loader>["match"]["mapList"]>;
|
||||
|
|
@ -1072,11 +1040,8 @@ function MapListMap({
|
|||
setWinners?: (winners: ("ALPHA" | "BRAVO")[]) => void;
|
||||
canReportScore: boolean;
|
||||
weapons?: (MainWeaponId | null)[] | null;
|
||||
ownWeapon?: MainWeaponId | null;
|
||||
onOwnWeaponSelected?: (weapon: ReportedWeaponForMerging | null) => void;
|
||||
showReportedOwnWeapon: boolean;
|
||||
recentlyReportedWeapons?: MainWeaponId[];
|
||||
addRecentlyReportedWeapon?: (weapon: MainWeaponId) => void;
|
||||
}) {
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
|
@ -1230,15 +1195,6 @@ function MapListMap({
|
|||
<label className="mb-0 text-theme-secondary">
|
||||
{t("q:match.report.winnerLabel")}
|
||||
</label>
|
||||
<div className="stack items-center">
|
||||
<div
|
||||
className={clsx("q-match__result-dot", {
|
||||
"q-match__result-dot__won": winners[i] === data.groupMemberOf,
|
||||
"q-match__result-dot__lost":
|
||||
winners[i] && winners[i] !== data.groupMemberOf,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="stack sm horizontal items-center">
|
||||
<div className="stack sm horizontal items-center font-semi-bold">
|
||||
<input
|
||||
|
|
@ -1273,68 +1229,24 @@ function MapListMap({
|
|||
<label className="mb-0 text-theme-secondary">
|
||||
{t("q:match.report.weaponLabel")}
|
||||
</label>
|
||||
<div
|
||||
className={clsx({ invisible: typeof ownWeapon !== "number" })}
|
||||
>
|
||||
{typeof ownWeapon === "number" ? (
|
||||
<WeaponImage
|
||||
weaponSplId={ownWeapon}
|
||||
variant="badge"
|
||||
size={36}
|
||||
/>
|
||||
) : (
|
||||
<WeaponImage
|
||||
weaponSplId={0}
|
||||
variant="badge"
|
||||
size={36}
|
||||
className="invisible"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{typeof ownWeapon === "number" ? (
|
||||
<div className="font-bold stack sm horizontal">
|
||||
{t(`weapons:MAIN_${ownWeapon}`)}
|
||||
<SendouButton
|
||||
size="small"
|
||||
icon={<CrossIcon />}
|
||||
variant="minimal-destructive"
|
||||
onPress={() => {
|
||||
const userId = user!.id;
|
||||
const groupMatchMapId = map.id;
|
||||
<WeaponSelect
|
||||
clearable
|
||||
onChange={(weaponSplId) => {
|
||||
const userId = user!.id;
|
||||
const groupMatchMapId = map.id;
|
||||
|
||||
onOwnWeaponSelected({
|
||||
mapIndex: i,
|
||||
groupMatchMapId,
|
||||
userId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
quickSelectWeaponIds={recentlyReportedWeapons}
|
||||
onChange={(weapon) => {
|
||||
const userId = user!.id;
|
||||
const groupMatchMapId = map.id;
|
||||
|
||||
const weaponSplId = Number(weapon?.value) as MainWeaponId;
|
||||
|
||||
addRecentlyReportedWeapon?.(weaponSplId);
|
||||
|
||||
onOwnWeaponSelected(
|
||||
weapon
|
||||
? {
|
||||
weaponSplId,
|
||||
mapIndex: i,
|
||||
groupMatchMapId,
|
||||
userId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onOwnWeaponSelected(
|
||||
typeof weaponSplId === "number"
|
||||
? {
|
||||
weaponSplId,
|
||||
mapIndex: i,
|
||||
groupMatchMapId,
|
||||
userId,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import * as React from "react";
|
|||
import { useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { ModeImage, WeaponImage } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
|
|
@ -29,7 +29,7 @@ import {
|
|||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { languagesUnified } from "~/modules/i18n/config";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import type { MainWeaponId, ModeShort } from "~/modules/in-game-lists/types";
|
||||
import type { ModeShort } from "~/modules/in-game-lists/types";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
|
|
@ -402,33 +402,27 @@ function WeaponPool() {
|
|||
/>
|
||||
<div className="q-settings__weapon-pool-select-container">
|
||||
{weapons.length < SENDOUQ_WEAPON_POOL_MAX_SIZE ? (
|
||||
<div>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
id="weapon"
|
||||
onChange={(weapon) => {
|
||||
if (!weapon) return;
|
||||
setWeapons([
|
||||
...weapons,
|
||||
{
|
||||
weaponSplId: Number(weapon.value) as MainWeaponId,
|
||||
isFavorite: 0,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
// empty on selection
|
||||
key={latestWeapon ?? "empty"}
|
||||
weaponIdsToOmit={new Set(weapons.map((w) => w.weaponSplId))}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<WeaponSelect
|
||||
onChange={(weaponSplId) => {
|
||||
setWeapons([
|
||||
...weapons,
|
||||
{
|
||||
weaponSplId,
|
||||
isFavorite: 0,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
// empty on selection
|
||||
key={latestWeapon ?? "empty"}
|
||||
disabledWeaponIds={weapons.map((w) => w.weaponSplId)}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-info">
|
||||
{t("q:settings.weaponPool.full")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="stack horizontal sm justify-center">
|
||||
<div className="stack horizontal md justify-center">
|
||||
{weapons.map((weapon) => {
|
||||
return (
|
||||
<div key={weapon.weaponSplId} className="stack xs">
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import * as React from "react";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
|
||||
const LOCAL_STORAGE_KEY = "sq__reported-weapons";
|
||||
const MAX_REPORTED_WEAPONS = 6;
|
||||
|
||||
export function useRecentlyReportedWeapons() {
|
||||
const [recentlyReportedWeapons, setReportedWeapons] = React.useState<
|
||||
MainWeaponId[]
|
||||
>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setReportedWeapons(getReportedWeaponsFromLocalStorage());
|
||||
}, []);
|
||||
|
||||
const addRecentlyReportedWeapon = (weapon: MainWeaponId) => {
|
||||
const newList = addReportedWeaponToLocalStorage(weapon);
|
||||
setReportedWeapons(newList);
|
||||
};
|
||||
|
||||
return { recentlyReportedWeapons, addRecentlyReportedWeapon };
|
||||
}
|
||||
|
||||
const getReportedWeaponsFromLocalStorage = (): MainWeaponId[] => {
|
||||
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (!stored) return [];
|
||||
return JSON.parse(stored);
|
||||
};
|
||||
|
||||
/** Adds weapon to list of recently reported weapons to local storage returning the current list */
|
||||
const addReportedWeaponToLocalStorage = (weapon: MainWeaponId) => {
|
||||
const stored = getReportedWeaponsFromLocalStorage();
|
||||
|
||||
const otherWeapons = stored.filter((storedWeapon) => storedWeapon !== weapon);
|
||||
|
||||
if (otherWeapons.length >= MAX_REPORTED_WEAPONS) {
|
||||
otherWeapons.pop();
|
||||
}
|
||||
|
||||
const newList = [weapon, ...otherWeapons];
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newList));
|
||||
|
||||
return newList;
|
||||
};
|
||||
|
|
@ -408,28 +408,13 @@
|
|||
|
||||
.q-match__report-section {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
row-gap: var(--s-2);
|
||||
column-gap: var(--s-2-5);
|
||||
align-items: center;
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.q-match__result-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 100%;
|
||||
background-color: var(--text-lighter);
|
||||
}
|
||||
|
||||
.q-match__result-dot__won {
|
||||
background-color: var(--theme-success);
|
||||
}
|
||||
|
||||
.q-match__result-dot__lost {
|
||||
background-color: var(--theme-error);
|
||||
}
|
||||
|
||||
.q-match__pool-pass-container {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ interface PlacementsTableProps {
|
|||
type?: "PLAYER_NAME" | "MODE_INFO";
|
||||
}
|
||||
|
||||
const TENTATEK_BRAND_ID = "B10";
|
||||
const TAKOROKA_BRAND_ID = "B11";
|
||||
|
||||
export function PlacementsTable({
|
||||
placements,
|
||||
type = "PLAYER_NAME",
|
||||
|
|
@ -46,7 +49,9 @@ export function PlacementsTable({
|
|||
: "Takoroka Division"
|
||||
}
|
||||
path={brandImageUrl(
|
||||
placement.region === "WEST" ? "tentatek" : "takoroka",
|
||||
placement.region === "WEST"
|
||||
? TENTATEK_BRAND_ID
|
||||
: TAKOROKA_BRAND_ID,
|
||||
)}
|
||||
width={24}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Form, useLoaderData } from "@remix-run/react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { RequiredHiddenInput } from "~/components/RequiredHiddenInput";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
|
|
@ -223,32 +223,24 @@ function WeaponPoolSelect({
|
|||
name={id}
|
||||
value={JSON.stringify(weapons)}
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor={id} required={required}>
|
||||
{label}
|
||||
</Label>
|
||||
{weapons.length < TOURNAMENT_SUB.WEAPON_POOL_MAX_SIZE ? (
|
||||
<>
|
||||
<WeaponCombobox
|
||||
inputName={id}
|
||||
id={id}
|
||||
onChange={(weapon) => {
|
||||
if (!weapon) return;
|
||||
setWeapons([...weapons, Number(weapon.value) as MainWeaponId]);
|
||||
}}
|
||||
// empty on selection
|
||||
key={weapons[weapons.length - 1]}
|
||||
weaponIdsToOmit={new Set([...weapons, ...otherWeapons])}
|
||||
fullWidth
|
||||
/>
|
||||
<FormMessage type="info">{infoText}</FormMessage>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-warning">
|
||||
{t("user:forms.errors.maxWeapons")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{weapons.length < TOURNAMENT_SUB.WEAPON_POOL_MAX_SIZE ? (
|
||||
<>
|
||||
<WeaponSelect
|
||||
label={label}
|
||||
onChange={(weaponId) => {
|
||||
setWeapons([...weapons, weaponId]);
|
||||
}}
|
||||
disabledWeaponIds={[...weapons, ...otherWeapons]}
|
||||
// empty on selection
|
||||
key={weapons[weapons.length - 1]}
|
||||
/>
|
||||
<FormMessage type="info">{infoText}</FormMessage>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-warning">
|
||||
{t("user:forms.errors.maxWeapons")}
|
||||
</span>
|
||||
)}
|
||||
{weapons.length > 0 ? (
|
||||
<div className="stack horizontal sm justify-center">
|
||||
{weapons.map((weapon) => {
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ import {
|
|||
LOG_IN_URL,
|
||||
SENDOU_INK_BASE_URL,
|
||||
SENDOU_INK_DISCORD_URL,
|
||||
mapsPageWithMapPool,
|
||||
navIconUrl,
|
||||
readonlyMapsPage,
|
||||
tournamentJoinPage,
|
||||
tournamentOrganizationPage,
|
||||
tournamentSubsPage,
|
||||
|
|
@ -1319,13 +1319,15 @@ function TOPickedMapPoolInfo() {
|
|||
|
||||
if (tournament.ctx.toSetMapPool.length === 0) return null;
|
||||
|
||||
const mapPool = new MapPool(tournament.ctx.toSetMapPool);
|
||||
|
||||
return (
|
||||
<Section title={t("calendar:forms.mapPool")}>
|
||||
<div className="event__map-pool-section">
|
||||
<MapPoolStages mapPool={new MapPool(tournament.ctx.toSetMapPool)} />
|
||||
<MapPoolStages mapPool={mapPool} />
|
||||
<LinkButton
|
||||
className="event__create-map-list-link"
|
||||
to={readonlyMapsPage(tournament.ctx.eventId)}
|
||||
to={mapsPageWithMapPool(mapPool)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
clothesMainSlotAbility,
|
||||
dbBoolean,
|
||||
falsyToNull,
|
||||
filterOutNullishMembers,
|
||||
headMainSlotAbility,
|
||||
id,
|
||||
processMany,
|
||||
|
|
@ -35,7 +36,6 @@ import {
|
|||
safeJSONParse,
|
||||
shoesMainSlotAbility,
|
||||
stackableAbility,
|
||||
toArray,
|
||||
weaponSplId,
|
||||
} from "~/utils/zod";
|
||||
|
||||
|
|
@ -60,18 +60,17 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
"Build to edit not found",
|
||||
);
|
||||
|
||||
const someGearIsMissing =
|
||||
!data["HEAD[value]"] || !data["CLOTHES[value]"] || !data["SHOES[value]"];
|
||||
const someGearIsMissing = !data.HEAD || !data.CLOTHES || !data.SHOES;
|
||||
|
||||
const commonArgs = {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
abilities: data.abilities as BuildAbilitiesTuple,
|
||||
headGearSplId: (someGearIsMissing ? -1 : data["HEAD[value]"])!,
|
||||
clothesGearSplId: (someGearIsMissing ? -1 : data["CLOTHES[value]"])!,
|
||||
shoesGearSplId: (someGearIsMissing ? -1 : data["SHOES[value]"])!,
|
||||
headGearSplId: (someGearIsMissing ? -1 : data.HEAD)!,
|
||||
clothesGearSplId: (someGearIsMissing ? -1 : data.CLOTHES)!,
|
||||
shoesGearSplId: (someGearIsMissing ? -1 : data.SHOES)!,
|
||||
modes: modesShort.filter((mode) => data[mode]),
|
||||
weaponSplIds: data["weapon[value]"],
|
||||
weaponSplIds: data.weapons,
|
||||
ownerId: user.id,
|
||||
private: data.private,
|
||||
};
|
||||
|
|
@ -115,11 +114,11 @@ const newBuildActionSchema = z.object({
|
|||
RM: z.preprocess(checkboxValueToBoolean, z.boolean()),
|
||||
CB: z.preprocess(checkboxValueToBoolean, z.boolean()),
|
||||
private: z.preprocess(checkboxValueToDbBoolean, dbBoolean),
|
||||
"weapon[value]": z.preprocess(
|
||||
processMany(toArray, removeDuplicatesZod),
|
||||
weapons: z.preprocess(
|
||||
processMany(safeJSONParse, filterOutNullishMembers, removeDuplicatesZod),
|
||||
z.array(weaponSplId).min(1).max(BUILD.MAX_WEAPONS_COUNT),
|
||||
),
|
||||
"HEAD[value]": z.preprocess(
|
||||
HEAD: z.preprocess(
|
||||
actualNumber,
|
||||
z
|
||||
.number()
|
||||
|
|
@ -130,7 +129,7 @@ const newBuildActionSchema = z.object({
|
|||
headGearIds.includes(val as (typeof headGearIds)[number]),
|
||||
),
|
||||
),
|
||||
"CLOTHES[value]": z.preprocess(
|
||||
CLOTHES: z.preprocess(
|
||||
actualNumber,
|
||||
z
|
||||
.number()
|
||||
|
|
@ -141,7 +140,7 @@ const newBuildActionSchema = z.object({
|
|||
clothesGearIds.includes(val as (typeof clothesGearIds)[number]),
|
||||
),
|
||||
),
|
||||
"SHOES[value]": z.preprocess(
|
||||
SHOES: z.preprocess(
|
||||
actualNumber,
|
||||
z
|
||||
.number()
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ import * as React from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { GearCombobox, WeaponCombobox } from "~/components/Combobox";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { GearSelect } from "~/components/GearSelect";
|
||||
import { Image } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { RequiredHiddenInput } from "~/components/RequiredHiddenInput";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import { PlusIcon } from "~/components/icons/Plus";
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
validatedBuildFromSearchParams,
|
||||
validatedWeaponIdFromSearchParams,
|
||||
} from "~/features/build-analyzer";
|
||||
import { BUILD } from "~/features/builds/builds-constants";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import type {
|
||||
|
|
@ -34,7 +36,6 @@ import type { SendouRouteHandle } from "~/utils/remix.server";
|
|||
import { modeImageUrl } from "~/utils/urls";
|
||||
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
|
||||
|
||||
import { BUILD } from "~/features/builds/builds-constants";
|
||||
import { action } from "../actions/u.$identifier.builds.new.server";
|
||||
import { loader } from "../loaders/u.$identifier.builds.new.server";
|
||||
export { loader, action };
|
||||
|
|
@ -86,6 +87,7 @@ export default function NewBuildPage() {
|
|||
abilities={abilities}
|
||||
setAbilities={setAbilities}
|
||||
/>
|
||||
<div /> {/* spacer */}
|
||||
<Abilities abilities={abilities} setAbilities={setAbilities} />
|
||||
<TitleInput />
|
||||
<DescriptionTextarea />
|
||||
|
|
@ -200,7 +202,7 @@ function WeaponsSelector() {
|
|||
const [searchParams] = useSearchParams();
|
||||
const { buildToEdit } = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["common", "weapons", "builds"]);
|
||||
const [weapons, setWeapons] = React.useState(
|
||||
const [weapons, setWeapons] = React.useState<Array<MainWeaponId | null>>(
|
||||
buildToEdit?.weapons.map((wpn) => wpn.weaponSplId) ?? [
|
||||
validatedWeaponIdFromSearchParams(searchParams),
|
||||
],
|
||||
|
|
@ -211,33 +213,28 @@ function WeaponsSelector() {
|
|||
<Label required htmlFor="weapon">
|
||||
{t("builds:forms.weapons")}
|
||||
</Label>
|
||||
<input type="hidden" name="weapons" value={JSON.stringify(weapons)} />
|
||||
<div className="stack sm">
|
||||
{weapons.map((weapon, i) => {
|
||||
return (
|
||||
<div key={i} className="stack horizontal sm items-center">
|
||||
<div>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
id="weapon"
|
||||
className="u__build-form__weapon"
|
||||
required
|
||||
onChange={(opt) =>
|
||||
opt &&
|
||||
setWeapons((weapons) => {
|
||||
const newWeapons = [...weapons];
|
||||
newWeapons[i] = Number(opt.value) as MainWeaponId;
|
||||
return newWeapons;
|
||||
})
|
||||
}
|
||||
initialWeaponId={weapon ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
<WeaponSelect
|
||||
onChange={(weaponId) =>
|
||||
setWeapons((weapons) => {
|
||||
const newWeapons = [...weapons];
|
||||
newWeapons[i] = weaponId;
|
||||
return newWeapons;
|
||||
})
|
||||
}
|
||||
initialValue={weapon ?? undefined}
|
||||
testId={`weapon-${i}`}
|
||||
/>
|
||||
{i === weapons.length - 1 && (
|
||||
<>
|
||||
<SendouButton
|
||||
size="small"
|
||||
isDisabled={weapons.length === BUILD.MAX_WEAPONS_COUNT}
|
||||
onPress={() => setWeapons((weapons) => [...weapons, 0])}
|
||||
onPress={() => setWeapons((weapons) => [...weapons, null])}
|
||||
icon={<PlusIcon />}
|
||||
data-testid="add-weapon-button"
|
||||
/>
|
||||
|
|
@ -276,56 +273,52 @@ function GearSelector({
|
|||
}) {
|
||||
const { buildToEdit, gearIdToAbilities } = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation("builds");
|
||||
|
||||
const initialGearId = () => {
|
||||
const [value, setValue] = React.useState(() => {
|
||||
const gearId = !buildToEdit
|
||||
? undefined
|
||||
? null
|
||||
: type === "HEAD"
|
||||
? buildToEdit.headGearSplId
|
||||
: type === "CLOTHES"
|
||||
? buildToEdit.clothesGearSplId
|
||||
: buildToEdit.shoesGearSplId;
|
||||
|
||||
if (gearId === -1) return undefined;
|
||||
if (gearId === -1) return null;
|
||||
|
||||
return gearId;
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label htmlFor={type}>{t(`forms.gear.${type}`)}</Label>
|
||||
<div>
|
||||
<GearCombobox
|
||||
gearType={type}
|
||||
inputName={type}
|
||||
id={type}
|
||||
initialGearId={initialGearId()}
|
||||
nullable
|
||||
// onChange only exists to copy abilities from existing gear
|
||||
// actual value of combobox is handled in uncontrolled manner
|
||||
onChange={(opt) => {
|
||||
if (!opt) return;
|
||||
<>
|
||||
<input type="hidden" name={type} value={value ?? ""} />
|
||||
<GearSelect
|
||||
label={t(`forms.gear.${type}`)}
|
||||
type={type}
|
||||
value={value}
|
||||
clearable
|
||||
onChange={(gearId) => {
|
||||
setValue(gearId);
|
||||
|
||||
const abilitiesFromExistingGear =
|
||||
gearIdToAbilities[`${type}_${opt.value}`];
|
||||
if (!gearId) return;
|
||||
|
||||
if (!abilitiesFromExistingGear) return;
|
||||
const abilitiesFromExistingGear =
|
||||
gearIdToAbilities[`${type}_${gearId}`];
|
||||
|
||||
const gearIndex = type === "HEAD" ? 0 : type === "CLOTHES" ? 1 : 2;
|
||||
if (!abilitiesFromExistingGear) return;
|
||||
|
||||
const currentAbilities = abilities[gearIndex];
|
||||
const gearIndex = type === "HEAD" ? 0 : type === "CLOTHES" ? 1 : 2;
|
||||
|
||||
// let's not overwrite current selections
|
||||
if (!currentAbilities.every((a) => a === "UNKNOWN")) return;
|
||||
const currentAbilities = abilities[gearIndex];
|
||||
|
||||
const newAbilities = structuredClone(abilities);
|
||||
newAbilities[gearIndex] = abilitiesFromExistingGear;
|
||||
// let's not overwrite current selections
|
||||
if (!currentAbilities.every((a) => a === "UNKNOWN")) return;
|
||||
|
||||
setAbilities(newAbilities);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
const newAbilities = structuredClone(abilities);
|
||||
newAbilities[gearIndex] = abilitiesFromExistingGear;
|
||||
|
||||
setAbilities(newAbilities);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Form, Link, useLoaderData, useMatches } from "@remix-run/react";
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { CustomizedColorsInput } from "~/components/CustomizedColorsInput";
|
||||
import { FormErrors } from "~/components/FormErrors";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
|
|
@ -10,6 +9,7 @@ import { WeaponImage } from "~/components/Image";
|
|||
import { Input } from "~/components/Input";
|
||||
import { Label } from "~/components/Label";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
|
|
@ -20,7 +20,6 @@ import type { Tables } from "~/db/tables";
|
|||
import { BADGE } from "~/features/badges/badges-constants";
|
||||
import { BadgesSelector } from "~/features/badges/components/BadgesSelector";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { rawSensToString } from "~/utils/strings";
|
||||
|
|
@ -215,6 +214,7 @@ function CountrySelect() {
|
|||
const { t, i18n } = useTranslation(["user"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const isMounted = useIsMounted();
|
||||
const [value, setValue] = React.useState(data.user.country ?? null);
|
||||
|
||||
const displayName = new Intl.DisplayNames(i18n.language, { type: "region" });
|
||||
|
||||
|
|
@ -230,22 +230,26 @@ function CountrySelect() {
|
|||
);
|
||||
|
||||
return (
|
||||
<SendouSelect
|
||||
items={items}
|
||||
label={t("user:country")}
|
||||
search={{
|
||||
placeholder: t("user:forms.country.search.placeholder"),
|
||||
}}
|
||||
name="country"
|
||||
defaultSelectedKey={data.user.country ?? undefined}
|
||||
className={styles.countrySelect}
|
||||
>
|
||||
{({ key, ...item }) => (
|
||||
<SendouSelectItem key={key} {...item}>
|
||||
{item.name}
|
||||
</SendouSelectItem>
|
||||
)}
|
||||
</SendouSelect>
|
||||
<>
|
||||
{/* TODO: this is a workaround for clearable not working with uncontrolled values, in future the component should handle this one way or another */}
|
||||
<input type="hidden" name="country" value={value ?? ""} />
|
||||
<SendouSelect
|
||||
items={items}
|
||||
label={t("user:country")}
|
||||
search={{
|
||||
placeholder: t("user:forms.country.search.placeholder"),
|
||||
}}
|
||||
selectedKey={value}
|
||||
onSelectionChange={(value) => setValue(value as string | null)}
|
||||
clearable
|
||||
>
|
||||
{({ key, ...item }) => (
|
||||
<SendouSelectItem key={key} {...item}>
|
||||
{item.name}
|
||||
</SendouSelectItem>
|
||||
)}
|
||||
</SendouSelect>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -278,33 +282,27 @@ function WeaponPoolSelect() {
|
|||
return (
|
||||
<div className={clsx("stack md", styles.weaponPool)}>
|
||||
<input type="hidden" name="weapons" value={JSON.stringify(weapons)} />
|
||||
<div>
|
||||
<label htmlFor="weapon">{t("user:weaponPool")}</label>
|
||||
{weapons.length < USER.WEAPON_POOL_MAX_SIZE ? (
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
id="weapon"
|
||||
onChange={(weapon) => {
|
||||
if (!weapon) return;
|
||||
setWeapons([
|
||||
...weapons,
|
||||
{
|
||||
weaponSplId: Number(weapon.value) as MainWeaponId,
|
||||
isFavorite: 0,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
// empty on selection
|
||||
key={latestWeapon?.weaponSplId ?? "empty"}
|
||||
weaponIdsToOmit={new Set(weapons.map((w) => w.weaponSplId))}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-warning">
|
||||
{t("user:forms.errors.maxWeapons")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{weapons.length < USER.WEAPON_POOL_MAX_SIZE ? (
|
||||
<WeaponSelect
|
||||
label={t("user:weaponPool")}
|
||||
onChange={(weaponSplId) => {
|
||||
setWeapons([
|
||||
...weapons,
|
||||
{
|
||||
weaponSplId,
|
||||
isFavorite: 0,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
disabledWeaponIds={weapons.map((w) => w.weaponSplId)}
|
||||
// empty on selection
|
||||
key={latestWeapon?.weaponSplId ?? "empty"}
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-warning">
|
||||
{t("user:forms.errors.maxWeapons")}
|
||||
</span>
|
||||
)}
|
||||
<div className="stack horizontal sm justify-center">
|
||||
{weapons.map((weapon) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
Controller,
|
||||
get,
|
||||
|
|
@ -9,10 +8,10 @@ import {
|
|||
} from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { z } from "zod/v4";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { UserSearch } from "~/components/elements/UserSearch";
|
||||
import { AddFieldButton } from "~/components/form/AddFieldButton";
|
||||
|
|
@ -20,7 +19,6 @@ import { RemoveFieldButton } from "~/components/form/RemoveFieldButton";
|
|||
import type { Tables } from "~/db/tables";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { Alert } from "../../../components/Alert";
|
||||
|
|
@ -266,7 +264,6 @@ function MatchesFieldset({
|
|||
canRemove: boolean;
|
||||
videoType: Tables["Video"]["type"];
|
||||
}) {
|
||||
const id = React.useId();
|
||||
const { t } = useTranslation(["vods", "game-misc"]);
|
||||
|
||||
return (
|
||||
|
|
@ -317,16 +314,14 @@ function MatchesFieldset({
|
|||
<div className="stack sm">
|
||||
{new Array(4).fill(null).map((_, i) => {
|
||||
return (
|
||||
<WeaponCombobox
|
||||
<WeaponSelect
|
||||
key={i}
|
||||
required
|
||||
fullWidth
|
||||
inputName={`player-${i}-weapon`}
|
||||
initialWeaponId={value[i]}
|
||||
onChange={(selected) => {
|
||||
if (!selected) return;
|
||||
isRequired
|
||||
testId={`player-${i}-weapon`}
|
||||
initialValue={value[i]}
|
||||
onChange={(weaponId) => {
|
||||
const weapons = [...value];
|
||||
weapons[i] = Number(selected.value) as MainWeaponId;
|
||||
weapons[i] = weaponId;
|
||||
|
||||
onChange(weapons);
|
||||
}}
|
||||
|
|
@ -342,18 +337,14 @@ function MatchesFieldset({
|
|||
{new Array(4).fill(null).map((_, i) => {
|
||||
const adjustedI = i + 4;
|
||||
return (
|
||||
<WeaponCombobox
|
||||
<WeaponSelect
|
||||
key={i}
|
||||
required
|
||||
fullWidth
|
||||
inputName={`player-${adjustedI}-weapon`}
|
||||
initialWeaponId={value[adjustedI]}
|
||||
onChange={(selected) => {
|
||||
if (!selected) return;
|
||||
isRequired
|
||||
testId={`player-${adjustedI}-weapon`}
|
||||
initialValue={value[adjustedI]}
|
||||
onChange={(weaponId) => {
|
||||
const weapons = [...value];
|
||||
weapons[adjustedI] = Number(
|
||||
selected.value,
|
||||
) as MainWeaponId;
|
||||
weapons[adjustedI] = weaponId;
|
||||
|
||||
onChange(weapons);
|
||||
}}
|
||||
|
|
@ -364,25 +355,13 @@ function MatchesFieldset({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Label required htmlFor={id}>
|
||||
{t("vods:forms.title.weapon")}
|
||||
</Label>
|
||||
<WeaponCombobox
|
||||
id={id}
|
||||
required
|
||||
fullWidth
|
||||
inputName={`match-${idx}-weapon`}
|
||||
initialWeaponId={value[0]}
|
||||
onChange={(selected) =>
|
||||
onChange(
|
||||
selected?.value
|
||||
? [Number(selected.value) as MainWeaponId]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<WeaponSelect
|
||||
label={t("vods:forms.title.weapon")}
|
||||
isRequired
|
||||
testId={`match-${idx}-weapon`}
|
||||
initialValue={value[0]}
|
||||
onChange={(weaponId) => onChange([weaponId])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import type { MetaFunction } from "@remix-run/node";
|
|||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddNewButton } from "~/components/AddNewButton";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import { stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
|
|
@ -144,17 +144,14 @@ function Filters({
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("vods:forms.title.weapon")}</Label>
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
initialWeaponId={weapon}
|
||||
onChange={(selected) => {
|
||||
addToSearchParams("weapon", selected?.value ?? "");
|
||||
}}
|
||||
nullable
|
||||
/>
|
||||
</div>
|
||||
<WeaponSelect
|
||||
label={t("vods:forms.title.weapon")}
|
||||
initialValue={weapon}
|
||||
onChange={(weaponId) => {
|
||||
addToSearchParams("weapon", weaponId ?? "");
|
||||
}}
|
||||
clearable
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label>{t("vods:forms.title.type")}</Label>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import useSWRImmutable from "swr/immutable";
|
||||
import type { EventsWithMapPoolsLoaderData } from "~/features/calendar/routes/map-pool-events";
|
||||
import type { PatronsListLoaderData } from "~/features/front-page/routes/patrons-list";
|
||||
import type { TrustersLoaderData } from "~/features/sendouq/routes/trusters";
|
||||
import type { WeaponUsageLoaderData } from "~/features/sendouq/routes/weapon-usage";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import {
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
GET_TRUSTERS_ROUTE,
|
||||
PATRONS_LIST_ROUTE,
|
||||
getWeaponUsage,
|
||||
|
|
@ -22,19 +20,6 @@ const fetcher = (key: string) => async (url: string) => {
|
|||
return res.json();
|
||||
};
|
||||
|
||||
export function useAllEventsWithMapPools() {
|
||||
const { data, error } = useSWRImmutable<EventsWithMapPoolsLoaderData>(
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
fetcher(GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE),
|
||||
);
|
||||
|
||||
return {
|
||||
events: data?.events,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useWeaponUsage(args: {
|
||||
userId: number;
|
||||
season: number;
|
||||
|
|
|
|||
23
app/modules/in-game-lists/brand-ids.ts
Normal file
23
app/modules/in-game-lists/brand-ids.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export const brandIds = [
|
||||
"B00",
|
||||
"B01",
|
||||
"B02",
|
||||
"B03",
|
||||
"B04",
|
||||
"B05",
|
||||
"B06",
|
||||
"B07",
|
||||
"B08",
|
||||
"B09",
|
||||
"B10",
|
||||
"B11",
|
||||
"B15",
|
||||
"B16",
|
||||
"B17",
|
||||
"B18",
|
||||
"B19",
|
||||
"B20",
|
||||
"B97",
|
||||
"B98",
|
||||
"B99",
|
||||
] as const;
|
||||
|
|
@ -1,79 +1,176 @@
|
|||
export const headGearIds = [
|
||||
1, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
|
||||
1012, 1014, 1018, 1019, 1020, 1021, 1023, 1024, 1025, 1026, 1027, 1028, 1029,
|
||||
1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 2000, 2001, 2002,
|
||||
2003, 2004, 2005, 2008, 2009, 2010, 2011, 2012, 2013, 3000, 3001, 3002, 3003,
|
||||
3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016,
|
||||
3017, 3018, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031,
|
||||
3032, 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 4011,
|
||||
4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020, 4021, 4022, 4023, 5000,
|
||||
5001, 5002, 5003, 5004, 5007, 5008, 5009, 5010, 5011, 5012, 6000, 6001, 6002,
|
||||
6003, 6004, 6005, 6006, 7000, 7002, 7004, 7005, 7006, 7007, 7008, 7009, 7011,
|
||||
7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 8000, 8001, 8002,
|
||||
8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011, 8012, 8014, 8015, 8016,
|
||||
8017, 8018, 8019, 9001, 9002, 9003, 9004, 9005, 9007, 9008, 9009, 10003,
|
||||
21000, 21001, 21002, 21003, 21004, 21005, 21007, 21008, 21009, 21010, 21011,
|
||||
21012, 21013, 21014, 21015, 21016, 21017, 21018, 21019, 21020, 21021, 21022,
|
||||
21023, 24000, 24001, 24002, 24003, 24004, 24005, 24006, 24007, 24008, 24009,
|
||||
24010, 24011, 24012, 24013, 24014, 24015, 24016, 24017, 24018, 24019, 24020,
|
||||
24021, 24022, 24023, 24024, 24025, 24026, 24027, 24028, 24029, 24030, 24031,
|
||||
24032, 24033, 24034, 24035, 24036, 24037, 24038, 24039, 24040, 25000, 25001,
|
||||
25002, 25003, 25004, 25005, 25006, 25007, 25008, 25009, 25010, 25011, 25012,
|
||||
25013, 25016, 25017, 25018, 25019, 25020, 25021, 25022, 27000, 27004, 27109,
|
||||
27306, 27307, 27311, 28000, 28001, 29200, 29201, 29202, 29203, 29204,
|
||||
] as const;
|
||||
export const headGearBrandGrouped = {
|
||||
B00: [
|
||||
1, 4008, 6005, 8004, 8007, 8015, 24000, 24001, 24002, 24003, 24004, 24005,
|
||||
24006, 24007, 24012, 24013, 24014, 24015, 24016, 24017, 24018, 24019, 24020,
|
||||
24021, 24022, 24023, 24024, 24025, 24026, 24027, 24028, 24029, 24030, 24031,
|
||||
24032, 24033, 24034, 24035, 24036, 24037, 24038, 24039, 24040, 28000, 28001,
|
||||
],
|
||||
B01: [1012, 3016, 6000, 7021, 9001, 9002, 9009],
|
||||
B02: [1010, 1018, 1020, 1021, 3009, 4018, 8002],
|
||||
B03: [1033, 1037, 3008, 3014, 4017, 4023],
|
||||
B04: [1007, 1008, 1009, 3003, 3004, 3006, 3012, 3013, 7006, 7008, 9004],
|
||||
B05: [
|
||||
2004, 3001, 3002, 3005, 4000, 4007, 5000, 5001, 5002, 5003, 5004, 5007,
|
||||
5008, 5009, 5010, 7002, 7007, 7009, 7011, 7013, 7014, 7015, 7016, 7017,
|
||||
7018, 8000, 8001, 8003, 8010, 8011, 8012,
|
||||
],
|
||||
B06: [1006, 1011, 1025, 2008, 3010, 4001, 4003, 4011, 6001, 8008, 9007, 9008],
|
||||
B07: [
|
||||
1000, 1003, 1004, 1005, 1023, 1026, 1029, 1030, 1036, 2003, 2005, 2011,
|
||||
2012, 4005, 4006, 4009, 4013, 6006, 7000, 7004, 7005, 7012,
|
||||
],
|
||||
B08: [1024, 1027, 1031, 2000, 2002, 3000, 3011, 3028, 3031, 4015, 4016],
|
||||
B09: [1001, 1038, 2001, 2010, 3025, 4002, 4004, 4010, 4012, 4014, 4019, 4022],
|
||||
B10: [1014, 3007, 6002, 7020, 9003, 9005, 24008, 24009, 24010, 24011],
|
||||
B11: [1002, 6003],
|
||||
B15: [2009, 3015, 3027, 8005],
|
||||
B16: [1019, 3022, 3024, 8006, 8009],
|
||||
B17: [3017, 3018, 3023, 6004, 8016, 8018, 10003],
|
||||
B18: [1034, 1035, 2013, 3030, 4020, 4021, 5011, 5012, 7019, 8019],
|
||||
B19: [1028, 1032, 3026],
|
||||
B20: [1039, 3021, 3029, 3032, 8014, 8017],
|
||||
B97: [
|
||||
21000, 21001, 21002, 21003, 21004, 21005, 21007, 21008, 21009, 21010, 21011,
|
||||
21012, 21013, 21014, 21015, 21016, 21017, 21018, 21019, 21020, 21021, 21022,
|
||||
21023,
|
||||
],
|
||||
B98: [
|
||||
27000, 27004, 27109, 27306, 27307, 27311, 29200, 29201, 29202, 29203, 29204,
|
||||
],
|
||||
B99: [
|
||||
25000, 25001, 25002, 25003, 25004, 25005, 25006, 25007, 25008, 25009, 25010,
|
||||
25011, 25012, 25013, 25016, 25017, 25018, 25019, 25020, 25021, 25022,
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const clothesGearIds = [
|
||||
2, 3, 1000, 1001, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
|
||||
1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1026, 1027,
|
||||
1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040,
|
||||
1041, 1042, 1043, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055,
|
||||
1056, 1057, 1058, 1059, 1060, 1062, 1063, 1066, 1067, 1068, 1069, 1070, 1071,
|
||||
1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1081, 1082, 1083, 1084, 1085,
|
||||
1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 2000, 2001, 2002,
|
||||
2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015,
|
||||
2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2026, 2027, 2028, 3000,
|
||||
3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3013, 4000,
|
||||
4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 5000, 5001, 5002,
|
||||
5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010, 5011, 5012, 5013, 5014, 5015,
|
||||
5016, 5017, 5018, 5019, 5020, 5021, 5022, 5023, 5024, 5025, 5026, 5027, 5028,
|
||||
5029, 5033, 5034, 5035, 5036, 5038, 5039, 5040, 5041, 5042, 5043, 5045, 5046,
|
||||
5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055, 5056, 5057, 5058, 5059,
|
||||
5060, 5061, 5062, 5063, 5064, 5065, 6000, 6001, 6003, 6004, 6005, 6006, 6007,
|
||||
6008, 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011,
|
||||
7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024,
|
||||
7025, 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011,
|
||||
8012, 8013, 8014, 8015, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025,
|
||||
8026, 8027, 8028, 8030, 8031, 8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039,
|
||||
8040, 8041, 8042, 8043, 9000, 9001, 9002, 9003, 9004, 9005, 9007, 9008, 9009,
|
||||
9010, 9011, 9012, 9013, 9014, 9015, 9016, 9017, 10000, 10001, 10002, 10004,
|
||||
10005, 10006, 10007, 10008, 10009, 10010, 10012, 10013, 10014, 10015, 21000,
|
||||
21001, 21002, 21003, 21004, 21005, 21006, 21008, 21009, 21010, 21011, 21012,
|
||||
21013, 21014, 21015, 21016, 21017, 25000, 25001, 25002, 25003, 25004, 25005,
|
||||
25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25014, 25015, 25017,
|
||||
25018, 25019, 25020, 25021, 25022, 26000, 27000, 27004, 27306, 27307, 27311,
|
||||
28000, 28001, 29200,
|
||||
] as const;
|
||||
export const headGearIds = Object.values(headGearBrandGrouped).flat();
|
||||
|
||||
export const shoesGearIds = [
|
||||
1, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
|
||||
1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024,
|
||||
1025, 1026, 1027, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2008, 2009, 2010,
|
||||
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2022, 2023, 2024,
|
||||
2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037,
|
||||
2038, 2039, 2040, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051,
|
||||
2052, 2053, 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010,
|
||||
3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023,
|
||||
3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031, 4000, 4001, 4002, 4003, 4007,
|
||||
4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020,
|
||||
4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 5000, 5001, 5002, 6000, 6001,
|
||||
6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014,
|
||||
6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024, 6025, 6026, 6027, 6028,
|
||||
6029, 6030, 7000, 7001, 7002, 7003, 7004, 8000, 8001, 8002, 8003, 8004, 8005,
|
||||
8006, 8007, 8008, 8009, 8010, 8011, 8013, 8014, 8015, 8016, 21001, 21002,
|
||||
21003, 21004, 21005, 21006, 21007, 21008, 21009, 23000, 23001, 23002, 23003,
|
||||
23004, 23005, 23006, 23007, 23008, 23009, 23010, 23011, 23012, 23013, 23014,
|
||||
23015, 23016, 25000, 25001, 25002, 25003, 25004, 25005, 25006, 25007, 25008,
|
||||
25009, 25010, 25011, 25012, 25013, 25014, 25015, 25018, 25019, 25020, 25021,
|
||||
25022, 27000, 27004, 27306, 27307, 27311, 28000, 28001, 29200,
|
||||
] as const;
|
||||
export const clothesGearBrandGrouped = {
|
||||
B00: [
|
||||
2, 1000, 1006, 1022, 1023, 1030, 1035, 1052, 1053, 1062, 1075, 1076, 1077,
|
||||
2008, 2010, 3000, 3001, 3002, 3003, 3005, 3013, 7001, 7002, 7006, 26000,
|
||||
28000, 28001,
|
||||
],
|
||||
B01: [
|
||||
1031, 1040, 1057, 1058, 1082, 1083, 2011, 2014, 2028, 3004, 4005, 5004,
|
||||
5008, 6000, 6001, 8004,
|
||||
],
|
||||
B02: [
|
||||
1007, 1008, 1041, 1051, 5016, 7016, 7017, 7020, 8001, 8018, 8024, 8030,
|
||||
8034, 8042, 9004, 9005, 10012, 10014,
|
||||
],
|
||||
B03: [
|
||||
1004, 1005, 1036, 1037, 1038, 1039, 1042, 1043, 2006, 2007, 5006, 5007,
|
||||
8000, 8010, 8011,
|
||||
],
|
||||
B04: [
|
||||
2001, 2004, 2018, 2019, 4004, 5003, 5014, 5015, 5024, 5025, 5026, 5028,
|
||||
5034, 7005, 8002, 8006, 8009, 8012, 10002, 10009,
|
||||
],
|
||||
B05: [3009, 5010, 5011, 5012, 5013, 5020, 5021, 5035, 5038, 5048, 8005],
|
||||
B06: [
|
||||
1010, 1020, 1021, 1026, 1029, 1034, 1046, 2023, 2024, 7003, 8008, 9002,
|
||||
9003, 10000, 10001,
|
||||
],
|
||||
B07: [
|
||||
1009, 1011, 1012, 1017, 1027, 1048, 1055, 1084, 1085, 1086, 1087, 1088,
|
||||
1089, 5065, 8027, 10006, 10007, 10010,
|
||||
],
|
||||
B08: [
|
||||
1018, 1019, 2000, 2003, 2005, 2013, 2016, 4000, 4003, 5005, 5009, 5040,
|
||||
5063, 7000, 7004, 8003, 8007, 8013, 8014, 8015, 8021, 8037, 8038, 10004,
|
||||
10008,
|
||||
],
|
||||
B09: [
|
||||
1015, 1016, 2009, 2012, 5000, 5002, 5017, 5019, 5027, 5039, 5051, 5052,
|
||||
5054, 5060, 9000, 9001, 9007, 9010, 9014,
|
||||
],
|
||||
B10: [
|
||||
1001, 1003, 1054, 1063, 1068, 1069, 1070, 3007, 3010, 3011, 4006, 6006,
|
||||
9009, 9016,
|
||||
],
|
||||
B11: [
|
||||
1013, 1014, 1049, 1050, 1078, 1079, 1081, 2002, 2021, 3006, 3008, 4001,
|
||||
4002, 4007, 5001, 5018, 5061, 6007, 6008, 7015, 7018, 7019, 8041,
|
||||
],
|
||||
B15: [
|
||||
1047, 1066, 1067, 2015, 2017, 2026, 2027, 5041, 5053, 5064, 7010, 7011,
|
||||
7012, 8017, 8020, 8022, 8023, 8025,
|
||||
],
|
||||
B16: [
|
||||
1033, 1056, 1059, 1060, 1095, 4008, 4009, 4010, 5033, 6003, 6004, 6005,
|
||||
7013, 9011, 9012, 10005,
|
||||
],
|
||||
B17: [
|
||||
1032, 5022, 5023, 5029, 5036, 5042, 5043, 5049, 5050, 5055, 5056, 7007,
|
||||
7008, 7009, 8019, 8026, 8028, 8035, 8036, 9008, 9013, 9015,
|
||||
],
|
||||
B18: [1092, 1093, 7021, 7022, 7023, 8032, 8039, 8043, 10013, 10015],
|
||||
B19: [1071, 1072, 1073, 1074, 1094, 5045, 5046, 5057, 5058, 5062],
|
||||
B20: [1090, 1091, 5047, 5059, 7024, 7025, 8031, 8033, 8040, 9017],
|
||||
B97: [
|
||||
21000, 21001, 21002, 21003, 21004, 21005, 21006, 21008, 21009, 21010, 21011,
|
||||
21012, 21013, 21014, 21015, 21016, 21017,
|
||||
],
|
||||
B98: [3, 1028, 2020, 2022, 7014, 27000, 27004, 27306, 27307, 27311, 29200],
|
||||
B99: [
|
||||
25000, 25001, 25002, 25003, 25004, 25005, 25006, 25007, 25008, 25009, 25010,
|
||||
25011, 25012, 25013, 25014, 25015, 25017, 25018, 25019, 25020, 25021, 25022,
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const clothesGearIds = Object.values(clothesGearBrandGrouped).flat();
|
||||
|
||||
export const shoesGearBrandGrouped = {
|
||||
B00: [28000, 28001],
|
||||
B01: [
|
||||
1003, 1005, 1007, 1015, 1016, 1026, 1027, 2000, 2001, 2003, 2006, 2010,
|
||||
2011, 2012, 3031, 4017, 4021, 4022,
|
||||
],
|
||||
B02: [
|
||||
1, 1001, 1006, 2002, 2004, 2005, 2040, 4000, 4001, 4002, 4003, 4027, 4028,
|
||||
7000, 7001, 7002, 7003, 23007, 23015,
|
||||
],
|
||||
B03: [
|
||||
4014, 6000, 6001, 6002, 6003, 6006, 6007, 6008, 6013, 6014, 6017, 6019,
|
||||
6020, 6025, 6028, 6030, 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007,
|
||||
8008, 8009, 8014, 8015, 23002,
|
||||
],
|
||||
B04: [1000, 1002, 1004, 1017, 1018, 1019, 4008, 23000, 23004],
|
||||
B05: [],
|
||||
B06: [4011, 4012],
|
||||
B07: [],
|
||||
B08: [1008, 1009, 1014, 2008, 2009, 2013, 6012, 6018],
|
||||
B09: [
|
||||
1012, 1013, 3028, 3029, 4007, 4009, 4010, 4013, 5000, 5001, 5002, 6004,
|
||||
6005, 6009,
|
||||
],
|
||||
B10: [
|
||||
1021, 2014, 2015, 2019, 2023, 2024, 2029, 2030, 2031, 2032, 2033, 2037,
|
||||
2043, 2044, 3000, 3002, 3004, 3005, 3006, 3007, 3009, 3010, 3011, 3012,
|
||||
3014, 3018, 3020, 3021, 3022, 3030, 6010, 6011,
|
||||
],
|
||||
B11: [
|
||||
1010, 1011, 2016, 2020, 2028, 2034, 2035, 2039, 2042, 3001, 3003, 3008,
|
||||
3019, 3024, 4015, 4018, 23003, 23006,
|
||||
],
|
||||
B15: [4019, 6016, 6023, 6024, 8010, 8011, 8016],
|
||||
B16: [
|
||||
2017, 2018, 2022, 2025, 2026, 2027, 2038, 2050, 2051, 2053, 3015, 3016,
|
||||
3017, 3026, 23001, 23005, 23008, 23009, 23010,
|
||||
],
|
||||
B17: [1020, 1023, 1025, 2036, 2045, 2046, 2052, 3013],
|
||||
B18: [
|
||||
2047, 2048, 4023, 4024, 4025, 4026, 6027, 7004, 23011, 23012, 23013, 23016,
|
||||
],
|
||||
B19: [1022, 1024, 2049, 6021, 6022, 6026, 6029, 8013, 23014],
|
||||
B20: [3023, 3025, 3027, 4016, 4020],
|
||||
B97: [21001, 21002, 21003, 21004, 21005, 21006, 21007, 21008, 21009],
|
||||
B98: [27000, 27004, 27306, 27307, 27311, 29200],
|
||||
B99: [
|
||||
25000, 25001, 25002, 25003, 25004, 25005, 25006, 25007, 25008, 25009, 25010,
|
||||
25011, 25012, 25013, 25014, 25015, 25018, 25019, 25020, 25021, 25022,
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const shoesGearIds = Object.values(shoesGearBrandGrouped).flat();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { abilities } from "./abilities";
|
||||
import type { brandIds } from "./brand-ids";
|
||||
import type { modesShort, modesShortWithSpecial } from "./modes";
|
||||
import type { stageIds } from "./stage-ids";
|
||||
import type {
|
||||
|
|
@ -48,3 +49,5 @@ export type BuildAbilitiesTupleWithUnknown = [
|
|||
s3: AbilityWithUnknown,
|
||||
],
|
||||
];
|
||||
|
||||
export type BrandId = (typeof brandIds)[number];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,37 @@
|
|||
import type { AnyWeapon } from "~/features/build-analyzer";
|
||||
import {
|
||||
allWeaponAltNames,
|
||||
weaponAltNames,
|
||||
} from "~/modules/in-game-lists/weapon-alt-names";
|
||||
import { abilities } from "./abilities";
|
||||
import type { Ability } from "./types";
|
||||
|
||||
export function isAbility(value: string): value is Ability {
|
||||
return Boolean(abilities.some((a) => a.name === value));
|
||||
}
|
||||
|
||||
const normalizeTerm = (term: string): string => {
|
||||
return term.trim().toLocaleLowerCase();
|
||||
};
|
||||
|
||||
export function filterWeapon({
|
||||
weapon,
|
||||
weaponName,
|
||||
searchTerm,
|
||||
}: {
|
||||
weapon: AnyWeapon;
|
||||
weaponName: string;
|
||||
searchTerm: string;
|
||||
}): boolean {
|
||||
const normalizedSearchTerm = normalizeTerm(searchTerm);
|
||||
const normalizedWeaponName = normalizeTerm(weaponName);
|
||||
|
||||
const isAlt = allWeaponAltNames.has(normalizedSearchTerm);
|
||||
if (weapon.type === "MAIN" && isAlt) {
|
||||
return (
|
||||
weaponAltNames.get(weapon.id)?.includes(normalizedSearchTerm) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedWeaponName.includes(normalizedSearchTerm);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,3 +76,9 @@ export const weaponAltNames = new Map<MainWeaponId, string[] | string>()
|
|||
.set(8010, ["sword", "vwiper"])
|
||||
.set(8011, ["sword", "diper", "dwiper"])
|
||||
.set(8012, ["rust", "kwiper", "barazushi"]);
|
||||
|
||||
export const allWeaponAltNames = new Set(
|
||||
Array.from(weaponAltNames.values()).flatMap((name) =>
|
||||
typeof name === "string" ? [name] : name,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ export default [
|
|||
":id/report-winners",
|
||||
"features/calendar/routes/calendar.$id.report-winners.tsx",
|
||||
),
|
||||
route("map-pool-events", "features/calendar/routes/map-pool-events.ts"),
|
||||
]),
|
||||
route("/calendar.ics", "features/calendar/routes/calendar.ics.tsx"),
|
||||
|
||||
|
|
|
|||
|
|
@ -417,83 +417,6 @@ abbr[title] {
|
|||
font-size: var(--fonts-md);
|
||||
}
|
||||
|
||||
.combobox-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
width: var(--input-width, 12rem);
|
||||
}
|
||||
|
||||
.combobox-input.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.combobox-options {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: var(--input-width, 12rem);
|
||||
border-radius: var(--rounded);
|
||||
margin-top: var(--s-2);
|
||||
background-color: var(--bg-darker);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
padding-block: var(--s-3);
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.combobox-options.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.combobox-options.empty {
|
||||
padding-block: var(--s-1-5);
|
||||
}
|
||||
|
||||
.combobox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--s-1) var(--s-3);
|
||||
gap: var(--s-2);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.combobox-item-image {
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.combobox-item-label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.combobox-no-matches {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.combobox-emoji {
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.combobox-item.active {
|
||||
background-color: var(--theme-transparent);
|
||||
}
|
||||
|
||||
.combobox-username {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article > p {
|
||||
padding-block: var(--s-2-5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
gap: var(--s-6);
|
||||
}
|
||||
|
||||
.countrySelect {
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
.inGameNameText {
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,10 @@
|
|||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.pt-0-5-forced {
|
||||
padding-block-start: var(--s-0-5) !important;
|
||||
}
|
||||
|
||||
.pt-8-forced {
|
||||
padding-block-start: var(--s-8) !important;
|
||||
}
|
||||
|
|
@ -235,7 +239,7 @@
|
|||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-block-start: var(--s-0);
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.-mt-1px {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,18 @@ import { tournamentBracketsPage } from "./urls";
|
|||
export async function selectWeapon({
|
||||
page,
|
||||
name,
|
||||
inputName = "weapon",
|
||||
testId = "weapon-select",
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
inputName?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
return selectComboboxValue({ page, value: name, inputName });
|
||||
await page.getByTestId(testId).click();
|
||||
await page.getByPlaceholder("Search weapons...").fill(name);
|
||||
await page
|
||||
.getByRole("listbox", { name: "Suggestions" })
|
||||
.getByTestId(`weapon-select-option-${name}`)
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function selectUser({
|
||||
|
|
@ -38,26 +43,6 @@ export async function selectUser({
|
|||
await page.keyboard.press("Enter");
|
||||
}
|
||||
|
||||
export async function selectComboboxValue({
|
||||
page,
|
||||
value,
|
||||
inputName,
|
||||
locator,
|
||||
}: {
|
||||
page: Page;
|
||||
value: string;
|
||||
inputName?: string;
|
||||
locator?: Locator;
|
||||
}) {
|
||||
if (!locator && !inputName) {
|
||||
throw new Error("Must provide either locator or inputName");
|
||||
}
|
||||
const combobox = locator ?? page.getByTestId(`${inputName!}-combobox-input`);
|
||||
await combobox.clear();
|
||||
await combobox.fill(value);
|
||||
await combobox.press("Enter");
|
||||
}
|
||||
|
||||
/** page.goto that waits for the page to be hydrated before proceeding */
|
||||
export async function navigate({ page, url }: { page: Page; url: string }) {
|
||||
await page.goto(url);
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import type { ArtSource } from "~/features/art/art-types";
|
|||
import type { AuthErrorCode } from "~/features/auth/core/errors";
|
||||
import { serializeBuild } from "~/features/build-analyzer";
|
||||
import type { CalendarFilters } from "~/features/calendar/calendar-types";
|
||||
import type { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import type { StageBackgroundStyle } from "~/features/map-planner";
|
||||
import type { TierName } from "~/features/mmr/mmr-constants";
|
||||
import { JOIN_CODE_SEARCH_PARAM_KEY } from "~/features/sendouq/q-constants";
|
||||
import type { ModeShort } from "~/modules/in-game-lists/types";
|
||||
import type { BrandId, ModeShort } from "~/modules/in-game-lists/types";
|
||||
import type {
|
||||
Ability,
|
||||
AbilityWithUnknown,
|
||||
|
|
@ -139,7 +140,6 @@ export const THIRD_PLACEMENT_ICON_PATH =
|
|||
export const soundPath = (fileName: string) =>
|
||||
`/static-assets/sounds/${fileName}.wav`;
|
||||
|
||||
export const GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE = "/calendar/map-pool-events";
|
||||
export const GET_TRUSTERS_ROUTE = "/trusters";
|
||||
export const PATRONS_LIST_ROUTE = "/patrons-list";
|
||||
|
||||
|
|
@ -417,10 +417,8 @@ export const getWeaponUsage = ({
|
|||
return `/weapon-usage?userId=${userId}&season=${season}&modeShort=${modeShort}&stageId=${stageId}`;
|
||||
};
|
||||
|
||||
export const mapsPage = (eventId?: Tables["MapPoolMap"]["calendarEventId"]) =>
|
||||
`/maps${eventId ? `?eventId=${eventId}` : ""}`;
|
||||
export const readonlyMapsPage = (eventId: Tables["CalendarEvent"]["id"]) =>
|
||||
`/maps?readonly&eventId=${eventId}`;
|
||||
export const mapsPageWithMapPool = (mapPool: MapPool) =>
|
||||
`/maps?readonly&pool=${mapPool.serialized}`;
|
||||
export const articlePage = (slug: string) => `${ARTICLES_MAIN_PAGE}/${slug}`;
|
||||
export const analyzerPage = (args?: {
|
||||
weaponId: MainWeaponId;
|
||||
|
|
@ -491,12 +489,12 @@ export const specialWeaponVariantImageUrl = (
|
|||
`/static-assets/img/special-weapons/variants/${specialWeaponSplId}-${variant}`;
|
||||
export const abilityImageUrl = (ability: AbilityWithUnknown) =>
|
||||
`/static-assets/img/abilities/${ability}`;
|
||||
export const brandImageUrl = (brand: BrandId) =>
|
||||
`/static-assets/img/brands/${brand}`;
|
||||
export const modeImageUrl = (mode: ModeShortWithSpecial) =>
|
||||
`/static-assets/img/modes/${mode}`;
|
||||
export const stageImageUrl = (stageId: StageId) =>
|
||||
`/static-assets/img/stages/${stageId}`;
|
||||
export const brandImageUrl = (brand: "tentatek" | "takoroka") =>
|
||||
`/static-assets/img/layout/${brand}`;
|
||||
export const tierImageUrl = (tier: TierName | "CALCULATING") =>
|
||||
`/static-assets/img/tiers/${tier.toLowerCase()}`;
|
||||
export const preferenceEmojiUrl = (preference?: Preference) => {
|
||||
|
|
|
|||
|
|
@ -40,10 +40,8 @@ test.describe("Build Analyzer", () => {
|
|||
|
||||
// on new build page with preselected values
|
||||
await newBuildPrompt.click();
|
||||
await expect(page.getByTestId("HEAD-combobox-input")).toBeVisible();
|
||||
await expect(page.getByTestId("weapon-combobox-input")).toHaveValue(
|
||||
"Luna Blaster",
|
||||
);
|
||||
await expect(page.getByTestId("HEAD-gear-select")).toBeVisible();
|
||||
await expect(page.getByTestId("weapon-0")).toContainText("Luna Blaster");
|
||||
await page.getByTestId("SSU-ability").isVisible();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import { type Page, expect, test } from "@playwright/test";
|
||||
import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import type { GearType } from "~/db/tables";
|
||||
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
|
||||
import {
|
||||
impersonate,
|
||||
navigate,
|
||||
seed,
|
||||
selectComboboxValue,
|
||||
} from "~/utils/playwright";
|
||||
import { impersonate, navigate, seed, selectWeapon } from "~/utils/playwright";
|
||||
import { BUILDS_PAGE, userBuildsPage, userNewBuildPage } from "~/utils/urls";
|
||||
|
||||
test.describe("Builds", () => {
|
||||
|
|
@ -18,31 +14,31 @@ test.describe("Builds", () => {
|
|||
url: userNewBuildPage({ discordId: NZAP_TEST_DISCORD_ID }),
|
||||
});
|
||||
|
||||
await selectComboboxValue({
|
||||
locator: page.getByTestId("weapon-combobox-input").first(),
|
||||
value: "Tenta Brella",
|
||||
await selectWeapon({
|
||||
testId: "weapon-0",
|
||||
name: "Tenta Brella",
|
||||
page,
|
||||
});
|
||||
await page.getByTestId("add-weapon-button").click();
|
||||
await selectComboboxValue({
|
||||
locator: page.getByTestId("weapon-combobox-input").nth(1),
|
||||
value: "Splat Brella",
|
||||
await selectWeapon({
|
||||
testId: "weapon-1",
|
||||
name: "Splat Brella",
|
||||
page,
|
||||
});
|
||||
|
||||
await selectComboboxValue({
|
||||
inputName: "HEAD",
|
||||
value: "White Headband",
|
||||
await selectGear({
|
||||
type: "HEAD",
|
||||
name: "White Headband",
|
||||
page,
|
||||
});
|
||||
await selectComboboxValue({
|
||||
inputName: "CLOTHES",
|
||||
value: "Basic Tee",
|
||||
await selectGear({
|
||||
type: "CLOTHES",
|
||||
name: "Basic Tee",
|
||||
page,
|
||||
});
|
||||
await selectComboboxValue({
|
||||
inputName: "SHOES",
|
||||
value: "Blue Lo-Tops",
|
||||
await selectGear({
|
||||
type: "SHOES",
|
||||
name: "Blue Lo-Tops",
|
||||
page,
|
||||
});
|
||||
|
||||
|
|
@ -144,3 +140,20 @@ test.describe("Builds", () => {
|
|||
await expect(page.getByTestId("build-card")).toHaveCount(24);
|
||||
});
|
||||
});
|
||||
|
||||
async function selectGear({
|
||||
page,
|
||||
name,
|
||||
type,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
type: GearType;
|
||||
}) {
|
||||
await page.getByTestId(`${type}-gear-select`).click();
|
||||
await page.getByPlaceholder("Search gear...").fill(name);
|
||||
await page
|
||||
.getByRole("listbox", { name: "Suggestions" })
|
||||
.getByTestId(`gear-select-option-${name}`)
|
||||
.click();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ test.describe("VoDs page", () => {
|
|||
await selectWeapon({
|
||||
name: "Zink Mini Splatling",
|
||||
page,
|
||||
inputName: "match-0-weapon",
|
||||
testId: "match-0-weapon",
|
||||
});
|
||||
|
||||
await page.getByTestId("add-field-button").click();
|
||||
|
|
@ -63,7 +63,7 @@ test.describe("VoDs page", () => {
|
|||
await selectWeapon({
|
||||
name: "Tenta Brella",
|
||||
page,
|
||||
inputName: "match-1-weapon",
|
||||
testId: "match-1-weapon",
|
||||
});
|
||||
|
||||
await submit(page);
|
||||
|
|
@ -105,7 +105,7 @@ test.describe("VoDs page", () => {
|
|||
await selectWeapon({
|
||||
name: i < 4 ? "Luna Blaster" : "Tenta Brella",
|
||||
page,
|
||||
inputName: `player-${i}-weapon`,
|
||||
testId: `player-${i}-weapon`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ test.describe("VoDs page", () => {
|
|||
await selectWeapon({
|
||||
name: "Luna Blaster",
|
||||
page,
|
||||
inputName: "match-3-weapon",
|
||||
testId: "match-3-weapon",
|
||||
});
|
||||
|
||||
await submit(page);
|
||||
|
|
@ -149,8 +149,10 @@ test.describe("VoDs page", () => {
|
|||
url: VODS_PAGE,
|
||||
});
|
||||
|
||||
await page.getByText("N-ZAP").isVisible();
|
||||
const nzapUserPageLink = page.getByRole("link", { name: "N-ZAP" });
|
||||
|
||||
await nzapUserPageLink.isVisible();
|
||||
await selectWeapon({ page, name: "Carbon Roller" });
|
||||
await isNotVisible(page.getByText("N-ZAP"));
|
||||
await isNotVisible(nzapUserPageLink);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Vægt",
|
||||
"attribute.weight.Fast": "Let",
|
||||
"attribute.weight.Slow": "Tung",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@
|
|||
"forms.tags.cantFindExisting": "Kan du ikke finde en eksisterende etiket?",
|
||||
"forms.tags.addNew": "Lav en ny en",
|
||||
"forms.tags.addNew.placeholder": "Lav en ny etiket",
|
||||
"forms.tags.searchExisting.placeholder": "Søg i eksisterende etiketter",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "Maks antal etiketter opnået",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Tiebreak-banepulje",
|
||||
"maps.template": "Skabelon",
|
||||
"maps.template.none": "Ingen",
|
||||
"maps.template.event": "Begivenhed",
|
||||
"maps.template.presets": "Forudindstillet",
|
||||
"maps.template.yourRecentEvents": "Nylige begivenheder",
|
||||
"maps.template.preset.ANARCHY": "Anarchy-spiltilstande",
|
||||
|
|
@ -156,7 +155,10 @@
|
|||
"forms.errors.noSearchMatches": "Ingen resultater fundet",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "Noget gik galt. Prøv at genindlæse siden.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Særregler",
|
||||
"tag.name.ART": "Kunstpræmier",
|
||||
"tag.name.MONEY": "Pengepræmier",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "SquidForce",
|
||||
"BRAND_B01": "Zink",
|
||||
"BRAND_B02": "Krak-On",
|
||||
"BRAND_B03": "Rockenberg",
|
||||
"BRAND_B04": "Zekko",
|
||||
"BRAND_B05": "Forge",
|
||||
"BRAND_B06": "Firefin",
|
||||
"BRAND_B07": "Skalop",
|
||||
"BRAND_B08": "Splash Mob",
|
||||
"BRAND_B09": "Inkline",
|
||||
"BRAND_B10": "Tentatek",
|
||||
"BRAND_B11": "Takoroka",
|
||||
"BRAND_B15": "Annaki",
|
||||
"BRAND_B16": "Enperry",
|
||||
"BRAND_B17": "Toni Kensa",
|
||||
"BRAND_B18": "Z+F",
|
||||
"BRAND_B19": "Barazushi",
|
||||
"BRAND_B20": "Emberz",
|
||||
"BRAND_B97": "Grizzco",
|
||||
"BRAND_B98": "Cuttlegear",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Gewicht",
|
||||
"attribute.weight.Fast": "Leicht",
|
||||
"attribute.weight.Slow": "Schwer",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@
|
|||
"forms.tags.cantFindExisting": "",
|
||||
"forms.tags.addNew": "",
|
||||
"forms.tags.addNew.placeholder": "",
|
||||
"forms.tags.searchExisting.placeholder": "",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Arenen-Pool für Tie-Breaker",
|
||||
"maps.template": "Vorlage",
|
||||
"maps.template.none": "Keine",
|
||||
"maps.template.event": "Event",
|
||||
"maps.template.presets": "Voreinstellungen",
|
||||
"maps.template.yourRecentEvents": "Deine Events",
|
||||
"maps.template.preset.ANARCHY": "Anarchie-Modi",
|
||||
|
|
@ -156,7 +155,10 @@
|
|||
"forms.errors.noSearchMatches": "Keine Suchergebnisse",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "Etwas ist schiefgegangen. Versuche die Seite neu zu laden.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Spezielle Regeln",
|
||||
"tag.name.ART": "Kunst-Preise",
|
||||
"tag.name.MONEY": "Geld-Preise",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "TintactiX",
|
||||
"BRAND_B01": "Sagitron",
|
||||
"BRAND_B02": "Medusa",
|
||||
"BRAND_B03": "Rockberg",
|
||||
"BRAND_B04": "Zekko",
|
||||
"BRAND_B05": "Focus",
|
||||
"BRAND_B06": "Rilax",
|
||||
"BRAND_B07": "Jakomu",
|
||||
"BRAND_B08": "Gian",
|
||||
"BRAND_B09": "Alpomar",
|
||||
"BRAND_B10": "Tentatek",
|
||||
"BRAND_B11": "Kalamati",
|
||||
"BRAND_B15": "Annaki",
|
||||
"BRAND_B16": "Enperry",
|
||||
"BRAND_B17": "Toni Kensa",
|
||||
"BRAND_B18": "Zekkori",
|
||||
"BRAND_B19": "Barazushi",
|
||||
"BRAND_B20": "Shichiryn",
|
||||
"BRAND_B97": "Bär GmbH",
|
||||
"BRAND_B98": "Kuttel-Korps",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "Weapon to analyze",
|
||||
"attribute.weight": "Weight",
|
||||
"attribute.weight.Fast": "Light",
|
||||
"attribute.weight.Slow": "Heavy",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@
|
|||
"forms.tags.cantFindExisting": "Can't find an existing tag?",
|
||||
"forms.tags.addNew": "Create a new one.",
|
||||
"forms.tags.addNew.placeholder": "Create a new tag",
|
||||
"forms.tags.searchExisting.placeholder": "Search existing tags",
|
||||
"forms.tags.search.placeholder": "Search tags...",
|
||||
"forms.tags.placeholder": "Select a tag",
|
||||
"forms.tags.maxReached": "Max tags reached",
|
||||
"delete.title": "Are you sure you want to delete the art?",
|
||||
"unlink.title": "Are you sure you want to remove this art from your profile (only {{username}} can add it back)?"
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Tiebreaker map pool",
|
||||
"maps.template": "Template",
|
||||
"maps.template.none": "None",
|
||||
"maps.template.event": "Event",
|
||||
"maps.template.presets": "Presets",
|
||||
"maps.template.yourRecentEvents": "Recent Events",
|
||||
"maps.template.preset.ANARCHY": "Anarchy Modes",
|
||||
|
|
@ -156,7 +155,10 @@
|
|||
"forms.errors.noSearchMatches": "No matches found",
|
||||
"forms.userSearch.placeholder": "Search users by username, profile URL or Discord ID...",
|
||||
"forms.userSearch.noResults": "No users matching your search found",
|
||||
"errors.genericReload": "Something went wrong. Try reloading the page.",
|
||||
"forms.weaponSearch.placeholder": "Select a weapon",
|
||||
"forms.weaponSearch.search.placeholder": "Search weapons...",
|
||||
"forms.gearSearch.placeholder": "Select a gear",
|
||||
"forms.gearSearch.search.placeholder": "Search gear...",
|
||||
"tag.name.SPECIAL": "Special rules",
|
||||
"tag.name.ART": "Art prizes",
|
||||
"tag.name.MONEY": "Money prizes",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "Tableturf Battle",
|
||||
"GAME_S1": "Splatoon 1",
|
||||
"GAME_S2": "Splatoon 2",
|
||||
"GAME_S3": "Splatoon 3"
|
||||
"GAME_S3": "Splatoon 3",
|
||||
"BRAND_B00": "SquidForce",
|
||||
"BRAND_B01": "Zink",
|
||||
"BRAND_B02": "Krak-On",
|
||||
"BRAND_B03": "Rockenberg",
|
||||
"BRAND_B04": "Zekko",
|
||||
"BRAND_B05": "Forge",
|
||||
"BRAND_B06": "Firefin",
|
||||
"BRAND_B07": "Skalop",
|
||||
"BRAND_B08": "Splash Mob",
|
||||
"BRAND_B09": "Inkline",
|
||||
"BRAND_B10": "Tentatek",
|
||||
"BRAND_B11": "Takoroka",
|
||||
"BRAND_B15": "Annaki",
|
||||
"BRAND_B16": "Enperry",
|
||||
"BRAND_B17": "Toni Kensa",
|
||||
"BRAND_B18": "Z+F",
|
||||
"BRAND_B19": "Barazushi",
|
||||
"BRAND_B20": "Emberz",
|
||||
"BRAND_B97": "Grizzco",
|
||||
"BRAND_B98": "Cuttlegear",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Peso",
|
||||
"attribute.weight.Fast": "Ligero",
|
||||
"attribute.weight.Slow": "Pesado",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"forms.tags.cantFindExisting": "¿No eucuentras una etiqueta existente?",
|
||||
"forms.tags.addNew": "Crear nueva.",
|
||||
"forms.tags.addNew.placeholder": "Crear una nueva etiqueta",
|
||||
"forms.tags.searchExisting.placeholder": "Buscar etiquetas existentes",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "Limite máximo de etiquetas",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Grupo de mapas para desempate",
|
||||
"maps.template": "Plantilla",
|
||||
"maps.template.none": "Ninguno",
|
||||
"maps.template.event": "Evento",
|
||||
"maps.template.presets": "Presets",
|
||||
"maps.template.yourRecentEvents": "Eventos recientes",
|
||||
"maps.template.preset.ANARCHY": "Modos de combate caótico",
|
||||
|
|
@ -157,7 +156,10 @@
|
|||
"forms.errors.noSearchMatches": "No se encuentran partidos",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "Algo salió mal. Intente recargar la página.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Reglas especiales",
|
||||
"tag.name.ART": "Premios de arte",
|
||||
"tag.name.MONEY": "Premios de dinero",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "Kalamas",
|
||||
"BRAND_B01": "Tintaz",
|
||||
"BRAND_B02": "Meddux",
|
||||
"BRAND_B03": "Rockenberg",
|
||||
"BRAND_B04": "Ezko",
|
||||
"BRAND_B05": "Forima",
|
||||
"BRAND_B06": "Chokkor",
|
||||
"BRAND_B07": "Skalop",
|
||||
"BRAND_B08": "Jimmon",
|
||||
"BRAND_B09": "Moluskia",
|
||||
"BRAND_B10": "Arome",
|
||||
"BRAND_B11": "Sepioca",
|
||||
"BRAND_B15": "Annak",
|
||||
"BRAND_B16": "Krakien",
|
||||
"BRAND_B17": "Tonken",
|
||||
"BRAND_B18": "Ezkkor",
|
||||
"BRAND_B19": "Barazushi",
|
||||
"BRAND_B20": "Ascuax",
|
||||
"BRAND_B97": "Don Oso",
|
||||
"BRAND_B98": "Obvius",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Peso",
|
||||
"attribute.weight.Fast": "Ligero",
|
||||
"attribute.weight.Slow": "Pesado",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"forms.tags.cantFindExisting": "¿No eucuentras una etiqueta existente?",
|
||||
"forms.tags.addNew": "Crear nueva.",
|
||||
"forms.tags.addNew.placeholder": "Crear una nueva etiqueta",
|
||||
"forms.tags.searchExisting.placeholder": "Buscar etiquetas existentes",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "Limite máximo de etiquetas",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Grupo de escenarios para desempate",
|
||||
"maps.template": "Plantilla",
|
||||
"maps.template.none": "Ninguno",
|
||||
"maps.template.event": "Evento",
|
||||
"maps.template.presets": "Preajustes",
|
||||
"maps.template.yourRecentEvents": "Eventos recientes",
|
||||
"maps.template.preset.ANARCHY": "Estilos de combate caótico",
|
||||
|
|
@ -157,7 +156,10 @@
|
|||
"forms.errors.noSearchMatches": "No se encuentran partidos",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "Algo salió mal. Intente recargar la página.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Reglas especiales",
|
||||
"tag.name.ART": "Premios de arte",
|
||||
"tag.name.MONEY": "Premios de dinero",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "Kalamas",
|
||||
"BRAND_B01": "Tintaz",
|
||||
"BRAND_B02": "Meddux",
|
||||
"BRAND_B03": "Rockenberg",
|
||||
"BRAND_B04": "Zekko",
|
||||
"BRAND_B05": "Forima",
|
||||
"BRAND_B06": "Shachi",
|
||||
"BRAND_B07": "Skalop",
|
||||
"BRAND_B08": "Tinta comunal",
|
||||
"BRAND_B09": "Moluskia",
|
||||
"BRAND_B10": "Tentáctica",
|
||||
"BRAND_B11": "Pulpioka",
|
||||
"BRAND_B15": "Annak",
|
||||
"BRAND_B16": "Emperi",
|
||||
"BRAND_B17": "Tonken",
|
||||
"BRAND_B18": "ZyS",
|
||||
"BRAND_B19": "Barazushi",
|
||||
"BRAND_B20": "Ascuax",
|
||||
"BRAND_B97": "Don Oso",
|
||||
"BRAND_B98": "Sepichón",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Poids",
|
||||
"attribute.weight.Fast": "Léger",
|
||||
"attribute.weight.Slow": "Lourd",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"forms.tags.cantFindExisting": "Impossible de trouver un tag existant ?",
|
||||
"forms.tags.addNew": "En créer un nouveau.",
|
||||
"forms.tags.addNew.placeholder": "Créer un nouveau tag",
|
||||
"forms.tags.searchExisting.placeholder": "Chercher des tags existants",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "Limite de tags atteinte",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Pool de stages de tie breaker",
|
||||
"maps.template": "Modèle",
|
||||
"maps.template.none": "Aucun",
|
||||
"maps.template.event": "Événement",
|
||||
"maps.template.presets": "Préréglages",
|
||||
"maps.template.yourRecentEvents": "Événements récents",
|
||||
"maps.template.preset.ANARCHY": "Modes Anarchie",
|
||||
|
|
@ -157,7 +156,10 @@
|
|||
"forms.errors.noSearchMatches": "Pas de correspondance trouvée",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "Quelque chose s'est mal passé. Essayez d'actualiser la page.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Règles spéciales",
|
||||
"tag.name.ART": "Illustration à gagner",
|
||||
"tag.name.MONEY": "Argent à gagner",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "Kalmarus",
|
||||
"BRAND_B01": "Leviathus",
|
||||
"BRAND_B02": "Krakeurs",
|
||||
"BRAND_B03": "Iormungand",
|
||||
"BRAND_B04": "Ezko",
|
||||
"BRAND_B05": "Fosia",
|
||||
"BRAND_B06": "Arki",
|
||||
"BRAND_B07": "P. Tonk",
|
||||
"BRAND_B08": "Fréma",
|
||||
"BRAND_B09": "Abysma",
|
||||
"BRAND_B10": "Tentatek",
|
||||
"BRAND_B11": "Pionic",
|
||||
"BRAND_B15": "Aroz",
|
||||
"BRAND_B16": "DUX",
|
||||
"BRAND_B17": "Thony K",
|
||||
"BRAND_B18": "E+A",
|
||||
"BRAND_B19": "Alpaj",
|
||||
"BRAND_B20": "Tizonius",
|
||||
"BRAND_B97": "M. Ours Cie",
|
||||
"BRAND_B98": "Radmaré",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Poids",
|
||||
"attribute.weight.Fast": "Léger",
|
||||
"attribute.weight.Slow": "Lourd",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"forms.tags.cantFindExisting": "Impossible de trouver un tag existant ?",
|
||||
"forms.tags.addNew": "En créer un nouveau.",
|
||||
"forms.tags.addNew.placeholder": "Créer un nouveau tag",
|
||||
"forms.tags.searchExisting.placeholder": "Chercher des tags existants",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "Limite de tags atteinte",
|
||||
"delete.title": "Êtes vous sûre de vouloir supprimer cette œuvre?",
|
||||
"unlink.title": "Êtes vous sûre de vouloir supprimer cette œuvre de votre profil? (Seulement {{username}} pourra le remettre)"
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Pool de stages de tie breaker",
|
||||
"maps.template": "Modèle",
|
||||
"maps.template.none": "Aucun",
|
||||
"maps.template.event": "Événement",
|
||||
"maps.template.presets": "Préréglages",
|
||||
"maps.template.yourRecentEvents": "Événements récents",
|
||||
"maps.template.preset.ANARCHY": "Modes Anarchie",
|
||||
|
|
@ -157,7 +156,10 @@
|
|||
"forms.errors.noSearchMatches": "Pas de correspondance trouvée",
|
||||
"forms.userSearch.placeholder": "Cherchez les utilisateur par leur pseudo, l'URL de leur profil ou l'ID discord...",
|
||||
"forms.userSearch.noResults": "Aucun utilisateur trouvé",
|
||||
"errors.genericReload": "Quelque chose s'est mal passé. Essayez d'actualiser la page.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Règles spéciales",
|
||||
"tag.name.ART": "Illustration à gagner",
|
||||
"tag.name.MONEY": "Argent à gagner",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "Kalamarus Rex",
|
||||
"BRAND_B01": "Leviathus",
|
||||
"BRAND_B02": "Krakers",
|
||||
"BRAND_B03": "Iormungand",
|
||||
"BRAND_B04": "Ezko",
|
||||
"BRAND_B05": "Focus",
|
||||
"BRAND_B06": "Friture",
|
||||
"BRAND_B07": "Jack",
|
||||
"BRAND_B08": "Gédéon",
|
||||
"BRAND_B09": "Abysma",
|
||||
"BRAND_B10": "OculR",
|
||||
"BRAND_B11": "Cubic",
|
||||
"BRAND_B15": "Aroz",
|
||||
"BRAND_B16": "DUX",
|
||||
"BRAND_B17": "Thony K",
|
||||
"BRAND_B18": "E+F",
|
||||
"BRAND_B19": "Alpaj",
|
||||
"BRAND_B20": "Apex",
|
||||
"BRAND_B97": "M. Ours SA",
|
||||
"BRAND_B98": "Macalamar",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "משקל",
|
||||
"attribute.weight.Fast": "קל",
|
||||
"attribute.weight.Slow": "כבד",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"forms.tags.cantFindExisting": "לא מוצאים תג קיים?",
|
||||
"forms.tags.addNew": "יצירת חדש.",
|
||||
"forms.tags.addNew.placeholder": "יצירת תג חדש",
|
||||
"forms.tags.searchExisting.placeholder": "חיפוש תגים קיימים",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "הגעתם לכמות תגים מקסימלית",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "מאגר מפות לשובר שיוויון",
|
||||
"maps.template": "תבנית",
|
||||
"maps.template.none": "ללא",
|
||||
"maps.template.event": "אירוע",
|
||||
"maps.template.presets": "הגדרות קבועות מראש",
|
||||
"maps.template.yourRecentEvents": "אירועים אחרונים",
|
||||
"maps.template.preset.ANARCHY": "מודים של אנרכיה",
|
||||
|
|
@ -156,7 +155,10 @@
|
|||
"forms.errors.noSearchMatches": "לא נמצאה התאמה",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "משהו השתבש. נסו לטעון מחדש את העמוד.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "חוקים מיוחדים",
|
||||
"tag.name.ART": "פרסי ציור",
|
||||
"tag.name.MONEY": "פרסים בכסף",
|
||||
|
|
|
|||
|
|
@ -60,9 +60,30 @@
|
|||
"MODE_LONG_TC": "Tower Control",
|
||||
"MODE_LONG_RM": "Rainmaker",
|
||||
"MODE_LONG_CB": "Clam Blitz",
|
||||
"MODE_LONG_SR": "",
|
||||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"MODE_LONG_SR": "Salmon Run",
|
||||
"MODE_LONG_TB": "Tableturf Battle",
|
||||
"GAME_S1": "Splatoon 1",
|
||||
"GAME_S2": "Splatoon 2",
|
||||
"GAME_S3": "Splatoon 3",
|
||||
"BRAND_B00": "SquidForce",
|
||||
"BRAND_B01": "Zink",
|
||||
"BRAND_B02": "Krak-On",
|
||||
"BRAND_B03": "Rockenberg",
|
||||
"BRAND_B04": "Zekko",
|
||||
"BRAND_B05": "Forge",
|
||||
"BRAND_B06": "Firefin",
|
||||
"BRAND_B07": "Skalop",
|
||||
"BRAND_B08": "Splash Mob",
|
||||
"BRAND_B09": "Inkline",
|
||||
"BRAND_B10": "Tentatek",
|
||||
"BRAND_B11": "Takoroka",
|
||||
"BRAND_B15": "Annaki",
|
||||
"BRAND_B16": "Enperry",
|
||||
"BRAND_B17": "Toni Kensa",
|
||||
"BRAND_B18": "Z+F",
|
||||
"BRAND_B19": "Barazushi",
|
||||
"BRAND_B20": "Emberz",
|
||||
"BRAND_B97": "Grizzco",
|
||||
"BRAND_B98": "Cuttlegear",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "Peso",
|
||||
"attribute.weight.Fast": "Leggero",
|
||||
"attribute.weight.Slow": "Pesante",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
"forms.tags.cantFindExisting": "Non riesci a trovare un tag esistente?",
|
||||
"forms.tags.addNew": "Creane uno nuovo.",
|
||||
"forms.tags.addNew.placeholder": "Crea un nuovo tag",
|
||||
"forms.tags.searchExisting.placeholder": "Cerca tag esistenti",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "Numero massimo di tag raggiunto.",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "Pool scenari per lo spareggio",
|
||||
"maps.template": "Template",
|
||||
"maps.template.none": "Nessuno",
|
||||
"maps.template.event": "Evento",
|
||||
"maps.template.presets": "Preset",
|
||||
"maps.template.yourRecentEvents": "Eventi Recenti",
|
||||
"maps.template.preset.ANARCHY": "Modalità anarchia",
|
||||
|
|
@ -157,7 +156,10 @@
|
|||
"forms.errors.noSearchMatches": "Nessun risultato trovato.",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "Qualcosa è andato storto. Prova a ricaricare la pagina.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "Regole speciali",
|
||||
"tag.name.ART": "Premi artistici",
|
||||
"tag.name.MONEY": "Premi monetari",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "CalamaX",
|
||||
"BRAND_B01": "Totanic",
|
||||
"BRAND_B02": "Cnidarya",
|
||||
"BRAND_B03": "Rockenburg",
|
||||
"BRAND_B04": "Ezko",
|
||||
"BRAND_B05": "Fortia",
|
||||
"BRAND_B06": "Torryd",
|
||||
"BRAND_B07": "Capasaint",
|
||||
"BRAND_B08": "Chimmon",
|
||||
"BRAND_B09": "Maremont",
|
||||
"BRAND_B10": "Arome",
|
||||
"BRAND_B11": "Sepia",
|
||||
"BRAND_B15": "Annak",
|
||||
"BRAND_B16": "Imperial",
|
||||
"BRAND_B17": "Nero Marlin",
|
||||
"BRAND_B18": "E+T",
|
||||
"BRAND_B19": "BaraS",
|
||||
"BRAND_B20": "Car-bon",
|
||||
"BRAND_B97": "Ursus & Co",
|
||||
"BRAND_B98": "Krakensen",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "重さ",
|
||||
"attribute.weight.Fast": "軽量級",
|
||||
"attribute.weight.Slow": "重量級",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
"forms.tags.cantFindExisting": "タグが見つからない場合:",
|
||||
"forms.tags.addNew": "新規作成してください。",
|
||||
"forms.tags.addNew.placeholder": "新しいタグを作成",
|
||||
"forms.tags.searchExisting.placeholder": "既存のタグを検索する",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "タグの最大数に到達しました",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "タイブレークマップリスト",
|
||||
"maps.template": "テンプレート",
|
||||
"maps.template.none": "なし",
|
||||
"maps.template.event": "イベント",
|
||||
"maps.template.presets": "プリセット",
|
||||
"maps.template.yourRecentEvents": "最近のイベント",
|
||||
"maps.template.preset.ANARCHY": "バンカラマッチルール",
|
||||
|
|
@ -154,7 +153,10 @@
|
|||
"forms.errors.noSearchMatches": "検索結果がみつかりませんでした",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "エラーが発生しました。ページを再読込してください。",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "特別ルール",
|
||||
"tag.name.ART": "イラスト賞品",
|
||||
"tag.name.MONEY": "賞金",
|
||||
|
|
|
|||
|
|
@ -64,5 +64,26 @@
|
|||
"MODE_LONG_TB": "",
|
||||
"GAME_S1": "",
|
||||
"GAME_S2": "",
|
||||
"GAME_S3": ""
|
||||
"GAME_S3": "",
|
||||
"BRAND_B00": "バトロイカ",
|
||||
"BRAND_B01": "アイロニック",
|
||||
"BRAND_B02": "クラーゲス",
|
||||
"BRAND_B03": "ロッケンベルグ",
|
||||
"BRAND_B04": "エゾッコ",
|
||||
"BRAND_B05": "フォーリマ",
|
||||
"BRAND_B06": "ホッコリー",
|
||||
"BRAND_B07": "ホタックス",
|
||||
"BRAND_B08": "ジモン",
|
||||
"BRAND_B09": "シグレニ",
|
||||
"BRAND_B10": "アロメ",
|
||||
"BRAND_B11": "ヤコ",
|
||||
"BRAND_B15": "アナアキ",
|
||||
"BRAND_B16": "エンペリー",
|
||||
"BRAND_B17": "タタキケンサキ",
|
||||
"BRAND_B18": "エゾッコリー",
|
||||
"BRAND_B19": "バラズシ",
|
||||
"BRAND_B20": "シチリン",
|
||||
"BRAND_B97": "クマサン商会",
|
||||
"BRAND_B98": "アタリメイド",
|
||||
"BRAND_B99": "amiibo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@
|
|||
"banned.banModal.title": "",
|
||||
"banned.banModal.player": "",
|
||||
"banned.banModal.note": "",
|
||||
"banned.banModal.noteHelp": "",
|
||||
"banned.banModal.explanation": ""
|
||||
"banned.banModal.noteHelp": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"weaponSelect.label": "",
|
||||
"attribute.weight": "무게",
|
||||
"attribute.weight.Fast": "가벼움",
|
||||
"attribute.weight.Slow": "무거움",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
"forms.tags.cantFindExisting": "",
|
||||
"forms.tags.addNew": "",
|
||||
"forms.tags.addNew.placeholder": "",
|
||||
"forms.tags.searchExisting.placeholder": "",
|
||||
"forms.tags.search.placeholder": "",
|
||||
"forms.tags.placeholder": "",
|
||||
"forms.tags.maxReached": "",
|
||||
"delete.title": "",
|
||||
"unlink.title": ""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@
|
|||
"maps.tieBreakerMapPool": "타이브레이커 맵 풀",
|
||||
"maps.template": "템플릿",
|
||||
"maps.template.none": "없음",
|
||||
"maps.template.event": "이벤트",
|
||||
"maps.template.presets": "프리셋",
|
||||
"maps.template.yourRecentEvents": "최근 이벤트",
|
||||
"maps.template.preset.ANARCHY": "랭크 룰",
|
||||
|
|
@ -154,7 +153,10 @@
|
|||
"forms.errors.noSearchMatches": "검색 결과 없음",
|
||||
"forms.userSearch.placeholder": "",
|
||||
"forms.userSearch.noResults": "",
|
||||
"errors.genericReload": "뭔가 잘못됐습니다 페이지를 새로고침해보세요.",
|
||||
"forms.weaponSearch.placeholder": "",
|
||||
"forms.weaponSearch.search.placeholder": "",
|
||||
"forms.gearSearch.placeholder": "",
|
||||
"forms.gearSearch.search.placeholder": "",
|
||||
"tag.name.SPECIAL": "특별 규칙",
|
||||
"tag.name.ART": "그림 상품",
|
||||
"tag.name.MONEY": "상금",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user