mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 22:26:12 -05:00
WeaponSearch
This commit is contained in:
@@ -175,9 +175,9 @@
|
||||
}
|
||||
|
||||
.searchTypeRadioSelected {
|
||||
background-color: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
background-color: var(--color-accent-high);
|
||||
color: var(--color-text-inverse);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.searchTypeRadioHovered {
|
||||
@@ -227,6 +227,41 @@
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.weaponDestinationHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
padding: var(--s-3) var(--s-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.backButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-field);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text-high);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
background-color: var(--color-bg-higher);
|
||||
}
|
||||
|
||||
.selectedWeaponName {
|
||||
font-weight: var(--weight-semi);
|
||||
}
|
||||
|
||||
.destinationIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.searchKbd {
|
||||
display: none;
|
||||
|
||||
@@ -23,10 +23,19 @@ import {
|
||||
teamPage,
|
||||
tournamentOrganizationPage,
|
||||
userPage,
|
||||
weaponCategoryUrl,
|
||||
} from "~/utils/urls";
|
||||
import styles from "./CommandPalette.module.css";
|
||||
import {
|
||||
filterWeaponResults,
|
||||
getWeaponDestinationUrl,
|
||||
type SelectedWeapon,
|
||||
WeaponDestinationMenu,
|
||||
WeaponResultsList,
|
||||
} from "./WeaponSearch";
|
||||
|
||||
const SEARCH_TYPES = [
|
||||
"weapons",
|
||||
"users",
|
||||
"teams",
|
||||
"organizations",
|
||||
@@ -36,15 +45,21 @@ type SearchType = (typeof SEARCH_TYPES)[number];
|
||||
|
||||
const STORAGE_KEY = "command-palette-search-type";
|
||||
|
||||
const SEARCH_TYPE_ICONS: Record<SearchType, string> = {
|
||||
users: "u",
|
||||
teams: "t",
|
||||
organizations: "medal",
|
||||
tournaments: "calendar",
|
||||
};
|
||||
function searchTypeIconPath(type: SearchType): string {
|
||||
if (type === "weapons") {
|
||||
return weaponCategoryUrl("SHOOTERS");
|
||||
}
|
||||
const navIcons: Record<Exclude<SearchType, "weapons">, string> = {
|
||||
users: "u",
|
||||
teams: "t",
|
||||
organizations: "medal",
|
||||
tournaments: "calendar",
|
||||
};
|
||||
return navIconUrl(navIcons[type]);
|
||||
}
|
||||
|
||||
function getInitialSearchType(): SearchType {
|
||||
if (typeof window === "undefined") return "users";
|
||||
if (typeof window === "undefined") return "weapons";
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored && SEARCH_TYPES.includes(stored as SearchType)) {
|
||||
@@ -53,7 +68,7 @@ function getInitialSearchType(): SearchType {
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
return "users";
|
||||
return "weapons";
|
||||
}
|
||||
|
||||
export function CommandPalette() {
|
||||
@@ -136,12 +151,14 @@ function CommandPaletteContent({
|
||||
onClose: () => void;
|
||||
initialSearchType: SearchType | null;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { t } = useTranslation(["common", "weapons"]);
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [searchType, setSearchType] = React.useState<SearchType>(
|
||||
initialSearchType ?? getInitialSearchType(),
|
||||
);
|
||||
const [selectedWeapon, setSelectedWeapon] =
|
||||
React.useState<SelectedWeapon | null>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const listBoxRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -161,6 +178,7 @@ function CommandPaletteContent({
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
if (searchType === "weapons") return;
|
||||
if (!query) return;
|
||||
fetcher.load(
|
||||
`/search?q=${encodeURIComponent(query)}&type=${searchType}&limit=10`,
|
||||
@@ -173,7 +191,19 @@ function CommandPaletteContent({
|
||||
const results = fetcher.data?.results ?? [];
|
||||
const hasQuery = query.length > 0;
|
||||
|
||||
const weaponResults =
|
||||
searchType === "weapons" ? filterWeaponResults(query, t) : [];
|
||||
|
||||
const handleSelect = (key: React.Key) => {
|
||||
if (searchType === "weapons") {
|
||||
const weapon = weaponResults.find((w) => `weapon-${w.id}` === key);
|
||||
if (weapon) {
|
||||
setSelectedWeapon(weapon);
|
||||
setQuery("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = results.find((r) => getResultKey(r) === key);
|
||||
if (result) {
|
||||
navigate(getResultHref(result));
|
||||
@@ -183,15 +213,42 @@ function CommandPaletteContent({
|
||||
|
||||
const handleSearchTypeChange = (value: string) => {
|
||||
setSearchType(value as SearchType);
|
||||
setSelectedWeapon(null);
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "ArrowDown" && results.length > 0) {
|
||||
const currentResults = searchType === "weapons" ? weaponResults : results;
|
||||
if (e.key === "ArrowDown" && currentResults.length > 0) {
|
||||
e.preventDefault();
|
||||
listBoxRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDestinationSelect = (key: React.Key) => {
|
||||
if (!selectedWeapon) return;
|
||||
|
||||
const url = getWeaponDestinationUrl(key as string, selectedWeapon);
|
||||
if (url) {
|
||||
navigate(url);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToWeaponSearch = () => {
|
||||
setSelectedWeapon(null);
|
||||
};
|
||||
|
||||
if (searchType === "weapons" && selectedWeapon) {
|
||||
return (
|
||||
<WeaponDestinationMenu
|
||||
selectedWeapon={selectedWeapon}
|
||||
onBack={handleBackToWeaponSearch}
|
||||
onSelect={handleDestinationSelect}
|
||||
listBoxRef={listBoxRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
@@ -225,11 +282,7 @@ function CommandPaletteContent({
|
||||
[styles.searchTypeRadioFocusVisible]: isFocusVisible,
|
||||
})}
|
||||
>
|
||||
<Image
|
||||
path={navIconUrl(SEARCH_TYPE_ICONS[type])}
|
||||
size={18}
|
||||
alt=""
|
||||
/>
|
||||
<Image path={searchTypeIconPath(type)} size={18} alt="" />
|
||||
{t(`common:search.type.${type}`)}
|
||||
</span>
|
||||
)}
|
||||
@@ -237,32 +290,41 @@ function CommandPaletteContent({
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<ListBox
|
||||
ref={listBoxRef}
|
||||
className={styles.listBox}
|
||||
aria-label={t("common:search")}
|
||||
selectionMode="single"
|
||||
onAction={handleSelect}
|
||||
renderEmptyState={() =>
|
||||
hasQuery ? (
|
||||
<div className={styles.emptyState}>
|
||||
{t("common:search.noResults")}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyState}>{t("common:search.hint")}</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{results.map((result) => (
|
||||
<ListBoxItem
|
||||
key={getResultKey(result)}
|
||||
id={getResultKey(result)}
|
||||
className={styles.listBoxItem}
|
||||
>
|
||||
<ResultItem result={result} />
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
{searchType === "weapons" ? (
|
||||
<WeaponResultsList
|
||||
weaponResults={weaponResults}
|
||||
onSelect={handleSelect}
|
||||
hasQuery={hasQuery}
|
||||
listBoxRef={listBoxRef}
|
||||
/>
|
||||
) : (
|
||||
<ListBox
|
||||
ref={listBoxRef}
|
||||
className={styles.listBox}
|
||||
aria-label={t("common:search")}
|
||||
selectionMode="single"
|
||||
onAction={handleSelect}
|
||||
renderEmptyState={() =>
|
||||
hasQuery ? (
|
||||
<div className={styles.emptyState}>
|
||||
{t("common:search.noResults")}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyState}>{t("common:search.hint")}</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{results.map((result) => (
|
||||
<ListBoxItem
|
||||
key={getResultKey(result)}
|
||||
id={getResultKey(result)}
|
||||
className={styles.listBoxItem}
|
||||
>
|
||||
<ResultItem result={result} />
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
222
app/components/layout/WeaponSearch.tsx
Normal file
222
app/components/layout/WeaponSearch.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import {
|
||||
Calculator,
|
||||
ChartColumnBig,
|
||||
ChevronLeft,
|
||||
Flame,
|
||||
FlaskConical,
|
||||
ImageIcon,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { ListBox, ListBoxItem } from "react-aria-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
import { YouTubeIcon } from "~/components/icons/YouTube";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { filterWeapon } from "~/modules/in-game-lists/utils";
|
||||
import {
|
||||
mainWeaponIds,
|
||||
weaponIdToType,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import {
|
||||
ANALYZER_URL,
|
||||
artPage,
|
||||
LFG_PAGE,
|
||||
mainWeaponImageUrl,
|
||||
mySlugify,
|
||||
VODS_PAGE,
|
||||
weaponBuildPage,
|
||||
weaponBuildPopularPage,
|
||||
weaponBuildStatsPage,
|
||||
} from "~/utils/urls";
|
||||
import styles from "./CommandPalette.module.css";
|
||||
|
||||
export interface SelectedWeapon {
|
||||
id: MainWeaponId;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export function filterWeaponResults(
|
||||
query: string,
|
||||
t: TFunction<["common", "weapons"]>,
|
||||
): SelectedWeapon[] {
|
||||
if (!query) return [];
|
||||
|
||||
const matches: SelectedWeapon[] = [];
|
||||
for (const id of mainWeaponIds) {
|
||||
if (weaponIdToType(id) === "ALT_SKIN") continue;
|
||||
|
||||
const weaponName = t(`weapons:MAIN_${id}`);
|
||||
const isMatch = filterWeapon({
|
||||
weapon: { type: "MAIN", id },
|
||||
weaponName,
|
||||
searchTerm: query,
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
matches.push({
|
||||
id,
|
||||
name: weaponName,
|
||||
slug: mySlugify(weaponName),
|
||||
});
|
||||
}
|
||||
|
||||
if (matches.length >= 10) break;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function getWeaponDestinationUrl(
|
||||
key: string,
|
||||
weapon: SelectedWeapon,
|
||||
): string | null {
|
||||
const destinations: Record<string, string> = {
|
||||
builds: weaponBuildPage(weapon.slug),
|
||||
popular: weaponBuildPopularPage(weapon.slug),
|
||||
stats: weaponBuildStatsPage(weapon.slug),
|
||||
analyzer: `${ANALYZER_URL}?weapon=${weapon.id}`,
|
||||
vods: `${VODS_PAGE}?weapon=${weapon.id}`,
|
||||
art: artPage(weapon.slug),
|
||||
lfg: `${LFG_PAGE}?weapon=${weapon.id}`,
|
||||
};
|
||||
|
||||
return destinations[key] ?? null;
|
||||
}
|
||||
|
||||
export function WeaponDestinationMenu({
|
||||
selectedWeapon,
|
||||
onBack,
|
||||
onSelect,
|
||||
listBoxRef,
|
||||
}: {
|
||||
selectedWeapon: SelectedWeapon;
|
||||
onBack: () => void;
|
||||
onSelect: (key: React.Key) => void;
|
||||
listBoxRef: React.RefObject<HTMLDivElement | null>;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.weaponDestinationHeader}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backButton}
|
||||
onClick={onBack}
|
||||
aria-label={t("common:actions.back")}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<Image path={mainWeaponImageUrl(selectedWeapon.id)} size={24} alt="" />
|
||||
<span className={styles.selectedWeaponName}>{selectedWeapon.name}</span>
|
||||
</div>
|
||||
<ListBox
|
||||
ref={listBoxRef}
|
||||
className={styles.listBox}
|
||||
aria-label={selectedWeapon.name}
|
||||
onAction={onSelect}
|
||||
autoFocus
|
||||
>
|
||||
<ListBoxItem id="builds" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<FlaskConical size={20} />
|
||||
<span className={styles.resultName}>
|
||||
{t("common:pages.builds")}
|
||||
</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem id="popular" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<Flame size={20} />
|
||||
<span className={styles.resultName}>
|
||||
{t("common:pages.popularBuilds")}
|
||||
</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem id="stats" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<ChartColumnBig size={20} />
|
||||
<span className={styles.resultName}>
|
||||
{t("common:pages.abilityStats")}
|
||||
</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem id="analyzer" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<Calculator size={20} />
|
||||
<span className={styles.resultName}>
|
||||
{t("common:pages.analyzer")}
|
||||
</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem id="vods" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<YouTubeIcon className={styles.destinationIcon} />
|
||||
<span className={styles.resultName}>{t("common:pages.vods")}</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem id="art" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<ImageIcon size={20} />
|
||||
<span className={styles.resultName}>{t("common:pages.art")}</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
<ListBoxItem id="lfg" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
<Users size={20} />
|
||||
<span className={styles.resultName}>{t("common:pages.lfg")}</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function WeaponResultsList({
|
||||
weaponResults,
|
||||
onSelect,
|
||||
hasQuery,
|
||||
listBoxRef,
|
||||
}: {
|
||||
weaponResults: SelectedWeapon[];
|
||||
onSelect: (key: React.Key) => void;
|
||||
hasQuery: boolean;
|
||||
listBoxRef: React.RefObject<HTMLDivElement | null>;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
return (
|
||||
<ListBox
|
||||
ref={listBoxRef}
|
||||
className={styles.listBox}
|
||||
aria-label={t("common:search")}
|
||||
selectionMode="single"
|
||||
onAction={onSelect}
|
||||
renderEmptyState={() =>
|
||||
hasQuery ? (
|
||||
<div className={styles.emptyState}>
|
||||
{t("common:search.noResults")}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyState}>{t("common:search.hint")}</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{weaponResults.map((weapon) => (
|
||||
<ListBoxItem
|
||||
key={`weapon-${weapon.id}`}
|
||||
id={`weapon-${weapon.id}`}
|
||||
className={styles.listBoxItem}
|
||||
>
|
||||
<div className={styles.resultItem}>
|
||||
<Image path={mainWeaponImageUrl(weapon.id)} size={24} alt="" />
|
||||
<span className={styles.resultName}>{weapon.name}</span>
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
);
|
||||
}
|
||||
@@ -348,6 +348,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
"search.type.teams": "Teams",
|
||||
"search.type.organizations": "Organizations",
|
||||
"search.type.tournaments": "Tournaments",
|
||||
"search.type.weapons": "Weapons",
|
||||
"search.noResults": "No results found",
|
||||
"search.hint": "Start typing to search",
|
||||
"dataCredit.lean": "Data credit: Lean",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -344,6 +344,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -344,6 +344,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
@@ -344,6 +344,7 @@
|
||||
"search.type.teams": "",
|
||||
"search.type.organizations": "",
|
||||
"search.type.tournaments": "",
|
||||
"search.type.weapons": "",
|
||||
"search.noResults": "",
|
||||
"search.hint": "",
|
||||
"dataCredit.lean": "",
|
||||
|
||||
Reference in New Issue
Block a user