mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 07:37:55 -05:00
Add event search to map pool templates
This commit is contained in:
@@ -4,7 +4,7 @@ import Fuse from "fuse.js";
|
||||
import clsx from "clsx";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import type { GearType, UserWithPlusTier } from "~/db/types";
|
||||
import { useUsers } from "~/hooks/swr";
|
||||
import { useAllEventsWithMapPools, useUsers } from "~/hooks/swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
clothesGearIds,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "~/modules/in-game-lists";
|
||||
import { gearImageUrl, mainWeaponImageUrl } from "~/utils/urls";
|
||||
import { Image } from "./Image";
|
||||
import { type SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events";
|
||||
|
||||
const MAX_RESULTS_SHOWN = 6;
|
||||
|
||||
@@ -36,6 +37,7 @@ interface ComboboxProps<T> {
|
||||
initialValue?: ComboboxOption<T>;
|
||||
clearsInputOnFocus?: boolean;
|
||||
onChange?: (selectedOption?: ComboboxOption<T>) => void;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export function Combobox<T extends Record<string, string | null | number>>({
|
||||
@@ -49,11 +51,16 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
||||
className,
|
||||
id,
|
||||
isLoading = false,
|
||||
fullWidth = false,
|
||||
}: ComboboxProps<T>) {
|
||||
const [selectedOption, setSelectedOption] =
|
||||
React.useState<Unpacked<typeof options>>();
|
||||
const [lastSelectedOption, setLastSelectedOption] =
|
||||
React.useState<Unpacked<typeof options>>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedOption, setSelectedOption] = React.useState<
|
||||
Unpacked<typeof options> | undefined
|
||||
>(initialValue);
|
||||
const [lastSelectedOption, setLastSelectedOption] = React.useState<
|
||||
Unpacked<typeof options> | undefined
|
||||
>(initialValue);
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -75,73 +82,85 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
||||
|
||||
const noMatches = filteredOptions.length === 0;
|
||||
|
||||
const displayValue = (option: Unpacked<typeof options>) => {
|
||||
return option?.label ?? "";
|
||||
};
|
||||
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
value={selectedOption}
|
||||
onChange={(selected) => {
|
||||
onChange?.(selected);
|
||||
setSelectedOption(selected);
|
||||
setLastSelectedOption(selected);
|
||||
}}
|
||||
name={inputName}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<HeadlessCombobox.Input
|
||||
onFocus={() => {
|
||||
if (clearsInputOnFocus) {
|
||||
setSelectedOption(undefined);
|
||||
}
|
||||
<div className="combobox-wrapper">
|
||||
<HeadlessCombobox
|
||||
value={selectedOption}
|
||||
onChange={(selected) => {
|
||||
onChange?.(selected);
|
||||
setSelectedOption(selected);
|
||||
setLastSelectedOption(selected);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!selectedOption && clearsInputOnFocus) {
|
||||
setSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={isLoading ? "Loading..." : placeholder}
|
||||
className={clsx("combobox-input", className)}
|
||||
displayValue={(option) =>
|
||||
(option as unknown as Unpacked<typeof options>)?.label ?? ""
|
||||
}
|
||||
data-cy={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
required={required}
|
||||
/>
|
||||
<HeadlessCombobox.Options
|
||||
className={clsx("combobox-options", {
|
||||
empty: noMatches,
|
||||
hidden: !query,
|
||||
})}
|
||||
name={inputName}
|
||||
disabled={!selectedOption && isLoading}
|
||||
>
|
||||
{noMatches ? (
|
||||
<div className="combobox-no-matches">
|
||||
No matches found <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}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</li>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))
|
||||
)}
|
||||
</HeadlessCombobox.Options>
|
||||
</HeadlessCombobox>
|
||||
<HeadlessCombobox.Input
|
||||
onFocus={() => {
|
||||
if (clearsInputOnFocus) {
|
||||
setSelectedOption(undefined);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!selectedOption && clearsInputOnFocus) {
|
||||
setSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={isLoading ? t("actions.loading") : placeholder}
|
||||
className={clsx("combobox-input", className, {
|
||||
fullWidth,
|
||||
})}
|
||||
// To make SSR prefill work in an uncontrolled component
|
||||
defaultValue={initialValue ? displayValue(initialValue) : undefined}
|
||||
displayValue={displayValue}
|
||||
data-cy={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
required={required}
|
||||
/>
|
||||
<HeadlessCombobox.Options
|
||||
className={clsx("combobox-options", {
|
||||
empty: noMatches,
|
||||
fullWidth,
|
||||
hidden: !query,
|
||||
})}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</li>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))
|
||||
)}
|
||||
</HeadlessCombobox.Options>
|
||||
</HeadlessCombobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,6 +176,7 @@ export function UserCombobox({
|
||||
ComboboxProps<Pick<UserWithPlusTier, "discordId" | "plusTier">>,
|
||||
"inputName" | "onChange" | "className" | "id" | "required"
|
||||
> & { userIdsToOmit?: Set<number>; initialUserId?: number }) {
|
||||
const { t } = useTranslation();
|
||||
const { users, isLoading, isError } = useUsers();
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
@@ -181,9 +201,7 @@ export function UserCombobox({
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="text-sm text-error">
|
||||
Something went wrong. Try reloading the page.
|
||||
</div>
|
||||
<div className="text-sm text-error">{t("errors.genericReload")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +209,7 @@ export function UserCombobox({
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={options}
|
||||
placeholder="Sendou#0043"
|
||||
placeholder="Sendou#4059"
|
||||
isLoading={isLoading}
|
||||
initialValue={initialValue}
|
||||
onChange={onChange}
|
||||
@@ -288,3 +306,71 @@ export function GearCombobox({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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) => 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}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e && {
|
||||
id: parseInt(e.value, 10),
|
||||
name: e.label,
|
||||
serializedMapPool: e.serializedMapPool,
|
||||
}
|
||||
);
|
||||
}}
|
||||
className={className}
|
||||
id={id}
|
||||
required={required}
|
||||
isLoading={isLoading}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import { CrossIcon } from "./icons/Cross";
|
||||
import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft";
|
||||
import * as React from "react";
|
||||
import type { CalendarEvent } from "~/db/types";
|
||||
import type { SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events";
|
||||
import { assertType } from "~/utils/types";
|
||||
import { MapPoolEventsCombobox } from "./Combobox";
|
||||
import { useOnce } from "~/hooks/useOnce";
|
||||
|
||||
export type MapPoolSelectorProps = {
|
||||
mapPool: MapPool;
|
||||
@@ -24,9 +28,8 @@ export type MapPoolSelectorProps = {
|
||||
event?: Pick<CalendarEvent, "id" | "name">
|
||||
) => void;
|
||||
className?: string;
|
||||
recentEvents?: Array<
|
||||
Pick<CalendarEvent, "id" | "name"> & { serializedMapPool: string }
|
||||
>;
|
||||
recentEvents?: SerializedMapPoolEvent[];
|
||||
initialEvent?: Pick<CalendarEvent, "id" | "name">;
|
||||
};
|
||||
|
||||
export function MapPoolSelector({
|
||||
@@ -35,11 +38,20 @@ export function MapPoolSelector({
|
||||
handleRemoval,
|
||||
className,
|
||||
recentEvents,
|
||||
initialEvent,
|
||||
}: MapPoolSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [template, setTemplate] = React.useState<MapPoolTemplateValue>(
|
||||
detectTemplate(mapPool)
|
||||
initialEvent ? "event" : detectTemplate(mapPool)
|
||||
);
|
||||
|
||||
const initialSerializedEvent: SerializedMapPoolEvent | undefined = useOnce(
|
||||
() =>
|
||||
initialEvent && {
|
||||
...initialEvent,
|
||||
serializedMapPool: mapPool.serialized,
|
||||
}
|
||||
);
|
||||
|
||||
const handleStageModesChange = (newMapPool: MapPool) => {
|
||||
@@ -54,7 +66,8 @@ export function MapPoolSelector({
|
||||
|
||||
const handleTemplateChange = (template: MapPoolTemplateValue) => {
|
||||
setTemplate(template);
|
||||
if (template === "none") {
|
||||
|
||||
if (template === "none" || template === "event") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,6 +88,8 @@ export function MapPoolSelector({
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assertType<never, typeof template>();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -101,6 +116,12 @@ export function MapPoolSelector({
|
||||
handleChange={handleTemplateChange}
|
||||
recentEvents={recentEvents}
|
||||
/>
|
||||
{template === "event" && (
|
||||
<TemplateEventSelection
|
||||
initialEvent={initialSerializedEvent}
|
||||
handleEventChange={handleMapPoolChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MapPoolStages
|
||||
mapPool={mapPool}
|
||||
@@ -279,7 +300,8 @@ const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort];
|
||||
type MapPoolTemplateValue =
|
||||
| "none"
|
||||
| `preset:${MapModePresetId}`
|
||||
| `recent-event:${string}`;
|
||||
| `recent-event:${string}`
|
||||
| "event";
|
||||
|
||||
function detectTemplate(mapPool: MapPool): MapPoolTemplateValue {
|
||||
for (const presetId of presetIds) {
|
||||
@@ -313,6 +335,7 @@ 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}`}>
|
||||
@@ -340,3 +363,37 @@ function MapPoolTemplateSelect({
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
type TemplateEventSelectionProps = {
|
||||
handleEventChange: (
|
||||
mapPool: MapPool,
|
||||
event?: Pick<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>
|
||||
);
|
||||
}
|
||||
|
||||
18
app/db/models/calendar/findAllEventsWithMapPools.sql
Normal file
18
app/db/models/calendar/findAllEventsWithMapPools.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
select
|
||||
"CalendarEvent"."id",
|
||||
"CalendarEvent"."name",
|
||||
json_group_array(
|
||||
json_object(
|
||||
'stageId',
|
||||
"MapPoolMap"."stageId",
|
||||
'mode',
|
||||
"MapPoolMap"."mode"
|
||||
)
|
||||
) as "mapPool"
|
||||
from
|
||||
"CalendarEvent"
|
||||
join "MapPoolMap" on "CalendarEvent"."id" = "MapPoolMap"."calendarEventId"
|
||||
group by
|
||||
"CalendarEvent"."id"
|
||||
order by
|
||||
"CalendarEvent"."id" desc
|
||||
@@ -37,6 +37,7 @@ import createMapPoolMapSql from "./createMapPoolMap.sql";
|
||||
import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql";
|
||||
import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql";
|
||||
import findRecentMapPoolsByAuthorIdSql from "./findRecentMapPoolsByAuthorId.sql";
|
||||
import findAllEventsWithMapPoolsSql from "./findAllEventsWithMapPools.sql";
|
||||
|
||||
const createStm = sql.prepare(createSql);
|
||||
const updateStm = sql.prepare(updateSql);
|
||||
@@ -487,3 +488,18 @@ export function findRecentMapPoolsByAuthorId(
|
||||
serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)),
|
||||
}));
|
||||
}
|
||||
|
||||
const findAllEventsWithMapPoolsStm = sql.prepare(findAllEventsWithMapPoolsSql);
|
||||
export function findAllEventsWithMapPools() {
|
||||
return (
|
||||
findAllEventsWithMapPoolsStm.all() as Array<
|
||||
Pick<CalendarEvent, "id" | "name"> & {
|
||||
mapPool: string;
|
||||
}
|
||||
>
|
||||
).map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import type { EventsWithMapPoolsLoaderData } from "~/routes/calendar/map-pool-events";
|
||||
import type { UsersLoaderData } from "~/routes/users";
|
||||
|
||||
const ALL_USERS_ROUTE = "/users?_data=routes%2Fusers";
|
||||
import {
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
GET_ALL_USERS_ROUTE,
|
||||
} from "~/utils/urls";
|
||||
|
||||
const fetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
@@ -10,7 +13,7 @@ const fetcher = async (url: string) => {
|
||||
|
||||
export function useUsers() {
|
||||
const { data, error } = useSWRImmutable<UsersLoaderData>(
|
||||
ALL_USERS_ROUTE,
|
||||
GET_ALL_USERS_ROUTE,
|
||||
fetcher
|
||||
);
|
||||
|
||||
@@ -20,3 +23,16 @@ export function useUsers() {
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAllEventsWithMapPools() {
|
||||
const { data, error } = useSWRImmutable<EventsWithMapPoolsLoaderData>(
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
events: data?.events,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
12
app/routes/calendar/map-pool-events.ts
Normal file
12
app/routes/calendar/map-pool-events.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { json, type SerializeFrom } from "@remix-run/node";
|
||||
import { findAllEventsWithMapPools } from "~/db/models/calendar/queries.server";
|
||||
|
||||
export const loader = () => {
|
||||
return json({
|
||||
events: findAllEventsWithMapPools(),
|
||||
});
|
||||
};
|
||||
|
||||
export type EventsWithMapPoolsLoaderData = SerializeFrom<typeof loader>;
|
||||
export type SerializedMapPoolEvent =
|
||||
EventsWithMapPoolsLoaderData["events"][number];
|
||||
@@ -125,6 +125,7 @@ export default function MapListPage() {
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
recentEvents={data.recentEventsWithMapPools}
|
||||
initialEvent={data.calendarEvent}
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
|
||||
@@ -493,10 +493,18 @@ dialog::backdrop {
|
||||
}
|
||||
}
|
||||
|
||||
.combobox-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.combobox-input.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.combobox-options {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -510,6 +518,10 @@ dialog::backdrop {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.combobox-options.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.combobox-options.empty {
|
||||
padding-block: var(--s-1-5);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ export const COMMON_PREVIEW_IMAGE = "/img/layout/common-preview.png";
|
||||
export const ERROR_GIRL_IMAGE_PATH = `/img/layout/error-girl`;
|
||||
export const LOGO_PATH = `/img/layout/logo`;
|
||||
|
||||
export const GET_ALL_USERS_ROUTE = "/users";
|
||||
export const GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE = "/calendar/map-pool-events";
|
||||
|
||||
interface UserLinkArgs {
|
||||
discordId: User["discordId"];
|
||||
customUrl?: User["customUrl"];
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
"actions.delete": "Löschen",
|
||||
"actions.loadMore": "Mehr laden",
|
||||
"actions.close": "Schließen",
|
||||
"actions.loading": "Lädt...",
|
||||
"actions.clear": "Leeren",
|
||||
"actions.selectAll": "Alle auswählen",
|
||||
"actions.search": "Suchen",
|
||||
|
||||
"maps.createMapList": "Arenen-Liste erstellen",
|
||||
"maps.halfSz": "50% Herrschaft",
|
||||
@@ -43,6 +45,7 @@
|
||||
"maps.tournamentMaplist": "Arenen-Liste für Turnier erstellen (maps.iplabs.ink)",
|
||||
"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",
|
||||
@@ -54,6 +57,9 @@
|
||||
"forms.name": "Name",
|
||||
"forms.description": "Beschreibung",
|
||||
"forms.errors.title": "Diese Fehler müssen behoben werden",
|
||||
"forms.errors.noSearchMatches": "Keine Suchergebnisse",
|
||||
|
||||
"errors.genericReload": "Etwas ist schiefgegangen. Versuche die Seite neu zu laden.",
|
||||
|
||||
"tag.name.BADGE": "Abzeichen-Preise",
|
||||
"tag.name.SPECIAL": "Spezielle Regeln",
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
"actions.loadMore": "Load more",
|
||||
"actions.copyToClipboard": "Copy to clipboard",
|
||||
"actions.close": "Close",
|
||||
"actions.loading": "Loading...",
|
||||
"actions.clear": "Clear",
|
||||
"actions.selectAll": "Select All",
|
||||
"actions.search": "Search",
|
||||
|
||||
"maps.createMapList": "Create map list",
|
||||
"maps.halfSz": "50% SZ",
|
||||
@@ -45,6 +47,7 @@
|
||||
"maps.tournamentMaplist": "Create tournament map list (maps.iplabs.ink)",
|
||||
"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",
|
||||
@@ -56,6 +59,9 @@
|
||||
"forms.name": "Name",
|
||||
"forms.description": "Description",
|
||||
"forms.errors.title": "Following errors need to be fixed",
|
||||
"forms.errors.noSearchMatches": "No matches found",
|
||||
|
||||
"errors.genericReload": "Something went wrong. Try reloading the page.",
|
||||
|
||||
"tag.name.BADGE": "Badge prizes",
|
||||
"tag.name.SPECIAL": "Special rules",
|
||||
|
||||
Reference in New Issue
Block a user