From de9aefa996bf60eb048d7c3612a53f9e17f4bbac Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Sun, 23 Oct 2022 16:12:49 +0200 Subject: [PATCH 1/7] Unify MapPoolSelector components --- .../components/MapPoolSelector.tsx | 2 +- app/routes/calendar/$id/index.tsx | 2 +- app/routes/calendar/new.tsx | 2 +- app/routes/maps.tsx | 68 +------------------ 4 files changed, 5 insertions(+), 69 deletions(-) rename app/{routes/calendar => }/components/MapPoolSelector.tsx (97%) diff --git a/app/routes/calendar/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx similarity index 97% rename from app/routes/calendar/components/MapPoolSelector.tsx rename to app/components/MapPoolSelector.tsx index 62215babf..a9c54813c 100644 --- a/app/routes/calendar/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -19,7 +19,7 @@ export function MapPoolSelector({ stageId: StageId; }) => void; }) { - const { t } = useTranslation(["game-misc", "calendar"]); + const { t } = useTranslation(["game-misc"]); const isPresentational = !handleMapPoolChange; diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index 714e686c8..4e2b27a66 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -39,7 +39,7 @@ import { userPage, } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; -import { MapPoolSelector } from "../components/MapPoolSelector"; +import { MapPoolSelector } from "~/components/MapPoolSelector"; import { Tags } from "../components/Tags"; export const links: LinksFunction = () => { diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index af102411e..0c5c850cd 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -58,7 +58,7 @@ import { safeJSONParse, toArray, } from "~/utils/zod"; -import { MapPoolSelector } from "./components/MapPoolSelector"; +import { MapPoolSelector } from "~/components/MapPoolSelector"; import { Tags } from "./components/Tags"; const MIN_DATE = new Date(Date.UTC(2015, 4, 28)); diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index ff152df1a..2cf364247 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -7,20 +7,17 @@ import type { import type { ShouldReloadFunction } from "@remix-run/react"; import { Link } from "@remix-run/react"; import { useLoaderData, useSearchParams } from "@remix-run/react"; -import clsx from "clsx"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useCopyToClipboard } from "react-use"; import invariant from "tiny-invariant"; import { Button } from "~/components/Button"; -import { Image } from "~/components/Image"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import { Toggle } from "~/components/Toggle"; import { db } from "~/db"; import { i18next } from "~/modules/i18n"; import { - modes, stageIds, type ModeShort, type ModeWithStage, @@ -38,13 +35,9 @@ import { import type { MapPool } from "~/modules/map-pool-serializer/types"; import styles from "~/styles/maps.css"; import { makeTitle } from "~/utils/strings"; -import { - calendarEventPage, - ipLabsMaps, - modeImageUrl, - stageImageUrl, -} from "~/utils/urls"; +import { calendarEventPage, ipLabsMaps } from "~/utils/urls"; import { type SendouRouteHandle } from "~/utils/remix"; +import { MapPoolSelector } from "~/components/MapPoolSelector"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; @@ -180,63 +173,6 @@ function useSearchParamMapPool() { }; } -function MapPoolSelector({ - mapPool, - handleMapPoolChange, -}: { - mapPool: MapPool; - handleMapPoolChange: (args: { mode: ModeShort; stageId: StageId }) => void; -}) { - const { t } = useTranslation(["game-misc"]); - - return ( -
- {stageIds.map((stageId) => ( -
- -
-
{t(`game-misc:STAGE_${stageId}`)}
-
- {modes.map((mode) => { - const selected = mapPool[mode.short].includes(stageId); - - return ( - - ); - })} -
-
-
- ))} -
- ); -} - function MapListCreator({ mapPool }: { mapPool: MapPool }) { const { t } = useTranslation(["game-misc", "common"]); const [mapList, setMapList] = React.useState(); From 3b18598288d2c91adedc96865c84fab1f83b81bb Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Tue, 25 Oct 2022 22:31:05 +0200 Subject: [PATCH 2/7] Introduce utility MapPool class --- app/components/MapPoolSelector.tsx | 6 +- app/db/models/calendar/queries.server.ts | 4 +- app/modules/map-list-generator/map-list.ts | 11 ++- app/modules/map-list-generator/utils.ts | 6 +- app/modules/map-pool-serializer/index.ts | 8 +- app/modules/map-pool-serializer/map-pool.ts | 87 +++++++++++++++++++ .../map-pool-serializer/serializer.test.ts | 10 +-- app/modules/map-pool-serializer/serializer.ts | 16 ++-- app/modules/map-pool-serializer/types.ts | 5 +- app/routes/calendar/$id/index.tsx | 3 +- app/routes/calendar/new.tsx | 47 ++++------ app/routes/maps.tsx | 38 ++++---- 12 files changed, 159 insertions(+), 82 deletions(-) create mode 100644 app/modules/map-pool-serializer/map-pool.ts diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index a9c54813c..6896fd97f 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -26,7 +26,7 @@ export function MapPoolSelector({ const stageRowIsVisible = (stageId: StageId) => { if (!isPresentational) return true; - return modes.some((mode) => mapPool[mode.short].includes(stageId)); + return mapPool.hasStage(stageId); }; return ( @@ -44,9 +44,7 @@ export function MapPoolSelector({
{t(`game-misc:STAGE_${stageId}`)}
{modes.map((mode) => { - const selected = (mapPool[mode.short] as StageId[]).includes( - stageId - ); + const selected = mapPool.parsed[mode.short].includes(stageId); if (isPresentational && !selected) return null; if (isPresentational && selected) { diff --git a/app/db/models/calendar/queries.server.ts b/app/db/models/calendar/queries.server.ts index 270333f38..60ad44186 100644 --- a/app/db/models/calendar/queries.server.ts +++ b/app/db/models/calendar/queries.server.ts @@ -11,7 +11,7 @@ import type { CalendarEventResultPlayer, MapPoolMap, } from "../../types"; -import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator"; +import { MapPool } from "~/modules/map-pool-serializer"; import createSql from "./create.sql"; import updateSql from "./update.sql"; @@ -449,7 +449,7 @@ export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) { if (rows.length === 0) return; - return mapPoolListToMapPoolObject(rows); + return MapPool.parse(rows); } const eventsToReportStm = sql.prepare(eventsToReportSql); diff --git a/app/modules/map-list-generator/map-list.ts b/app/modules/map-list-generator/map-list.ts index 58f890d97..346a513ae 100644 --- a/app/modules/map-list-generator/map-list.ts +++ b/app/modules/map-list-generator/map-list.ts @@ -6,14 +6,13 @@ import type { ModeWithStage, StageId, } from "~/modules/in-game-lists"; -import type { MapPool } from "~/modules/map-pool-serializer"; -import clone from "just-clone"; +import type { MapPool, MapPoolObject } from "~/modules/map-pool-serializer"; const BACKLOG = 2; export type Popularity = Map>; -type MapBucket = Map; +type MapBucket = Map; /** * @param mapPool Map pool to work with as dictionary @@ -79,7 +78,7 @@ function addAndReturnMap( TC: [], RM: [], CB: [], - } as MapPool); + } as MapPoolObject); } /* prettier-ignore */ @@ -97,7 +96,7 @@ function getMapPopular( ): StageId { const popularity_map_pool = new Map(); for (const [stageId, votes] of popularity.get(mode)!.entries()) { - if (mapPool[mode].includes(stageId)) { + if (mapPool.parsed[mode].includes(stageId)) { popularity_map_pool.set(stageId, votes); } } @@ -150,7 +149,7 @@ function getMap( mapHistory: StageId[] ) { if (!buckets.size) { - buckets.set(0, clone(mapPool)); + buckets.set(0, mapPool.getClonedObject()); } for (let bucketNum = 0; bucketNum < buckets.size; bucketNum++) { diff --git a/app/modules/map-list-generator/utils.ts b/app/modules/map-list-generator/utils.ts index f804b87c0..bca57fe53 100644 --- a/app/modules/map-list-generator/utils.ts +++ b/app/modules/map-list-generator/utils.ts @@ -1,11 +1,11 @@ import type { MapPoolMap } from "~/db/types"; import type { ModeShort } from "../in-game-lists"; -import type { MapPool } from "../map-pool-serializer"; +import type { MapPool, MapPoolObject } from "../map-pool-serializer"; export function mapPoolToNonEmptyModes(mapPool: MapPool) { const result: ModeShort[] = []; - for (const [key, stages] of Object.entries(mapPool)) { + for (const [key, stages] of Object.entries(mapPool.parsed)) { if (stages.length === 0) continue; result.push(key as ModeShort); @@ -17,7 +17,7 @@ export function mapPoolToNonEmptyModes(mapPool: MapPool) { export function mapPoolListToMapPoolObject( mapPoolList: Array> ) { - const result: MapPool = { + const result: MapPoolObject = { TW: [], SZ: [], TC: [], diff --git a/app/modules/map-pool-serializer/index.ts b/app/modules/map-pool-serializer/index.ts index 79678884e..1a8ce98ee 100644 --- a/app/modules/map-pool-serializer/index.ts +++ b/app/modules/map-pool-serializer/index.ts @@ -1,6 +1,2 @@ -export { - mapPoolToSerializedString, - serializedStringToMapPool, -} from "./serializer"; - -export type { MapPool } from "./types"; +export { MapPool } from "./map-pool"; +export type { MapPoolObject } from "./types"; diff --git a/app/modules/map-pool-serializer/map-pool.ts b/app/modules/map-pool-serializer/map-pool.ts new file mode 100644 index 000000000..8c26c654b --- /dev/null +++ b/app/modules/map-pool-serializer/map-pool.ts @@ -0,0 +1,87 @@ +import { + mapPoolToSerializedString, + serializedStringToMapPool, +} from "./serializer"; +import type { ReadonlyMapPoolObject, MapPoolObject } from "./types"; +import clone from "just-clone"; +import type { MapPoolMap } from "~/db/types"; +import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator"; +import type { ModeShort, StageId } from "~/modules/in-game-lists"; + +type DbMapPoolList = Array>; + +export class MapPool { + private source: string | ReadonlyMapPoolObject; + private asSerialized?: string; + private asObject?: ReadonlyMapPoolObject; + + constructor(init: ReadonlyMapPoolObject | string | DbMapPoolList) { + this.source = Array.isArray(init) ? mapPoolListToMapPoolObject(init) : init; + } + + static serialize(init: ReadonlyMapPoolObject | string | DbMapPoolList) { + return new MapPool(init).serialized; + } + + static parse(init: MapPoolObject | string | DbMapPoolList) { + return new MapPool(init).parsed; + } + + static toDbList(init: MapPoolObject | string | DbMapPoolList) { + return new MapPool(init).dbList; + } + + get serialized(): string { + if (this.asSerialized !== undefined) { + return this.asSerialized; + } + + return (this.asSerialized = + typeof this.source === "string" + ? this.source + : mapPoolToSerializedString(this.source)); + } + + get parsed(): ReadonlyMapPoolObject { + if (this.asObject !== undefined) { + return this.asObject; + } + + return (this.asObject = + typeof this.source === "string" + ? serializedStringToMapPool(this.source) + : this.source); + } + + get dbList(): DbMapPoolList { + return Object.entries(this.parsed).flatMap(([mode, stages]) => + stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId })) + ); + } + + hasMode(mode: ModeShort): boolean { + return this.parsed[mode].length > 0; + } + + hasStage(stageId: StageId): boolean { + return Object.values(this.parsed).some((stages) => + stages.includes(stageId) + ); + } + + isEmpty(): boolean { + return Object.values(this.parsed).every((stages) => stages.length === 0); + } + + getClonedObject(): MapPoolObject { + return clone(this.parsed) as MapPoolObject; + } + + toString() { + return this.serialized; + } + + toJSON() { + return this.parsed; + } +} diff --git a/app/modules/map-pool-serializer/serializer.test.ts b/app/modules/map-pool-serializer/serializer.test.ts index 6487db55e..5d0b5aab0 100644 --- a/app/modules/map-pool-serializer/serializer.test.ts +++ b/app/modules/map-pool-serializer/serializer.test.ts @@ -4,7 +4,7 @@ import { mapPoolToSerializedString, serializedStringToMapPool, } from "./serializer"; -import type { MapPool } from "./types"; +import type { MapPoolObject } from "./types"; const Serializer = suite("Map pool serializer"); @@ -24,7 +24,7 @@ Serializer("Ignores invalid mode key", () => { }); Serializer("Matching serialization with IPLMapGen2", () => { - const testMapPool: MapPool = { + const testMapPool: MapPoolObject = { TW: [0, 3, 4, 7, 8], SZ: [0, 1, 3, 8, 10], TC: [1, 2, 5, 8, 9], @@ -36,7 +36,7 @@ Serializer("Matching serialization with IPLMapGen2", () => { }); Serializer("Omits key if mode has no maps", () => { - const testPoolWithoutTw: MapPool = { + const testPoolWithoutTw: MapPoolObject = { CB: [1, 2], RM: [1, 8], TC: [8, 4], @@ -50,7 +50,7 @@ Serializer("Omits key if mode has no maps", () => { }); Serializer("Returns empty string if no maps", () => { - const testPoolWithoutTw: MapPool = { + const testPoolWithoutTw: MapPoolObject = { CB: [], RM: [], TC: [], @@ -64,7 +64,7 @@ Serializer("Returns empty string if no maps", () => { }); Serializer("Value of two modes is the same with same maps", () => { - const testPoolWithDuplicateMaps: MapPool = { + const testPoolWithDuplicateMaps: MapPoolObject = { CB: [1, 2], RM: [1, 2], TC: [], diff --git a/app/modules/map-pool-serializer/serializer.ts b/app/modules/map-pool-serializer/serializer.ts index db2b47c01..c6bde377b 100644 --- a/app/modules/map-pool-serializer/serializer.ts +++ b/app/modules/map-pool-serializer/serializer.ts @@ -1,8 +1,10 @@ import invariant from "tiny-invariant"; import { modesShort, type StageId, stageIds } from "../in-game-lists"; -import type { MapPool } from "./types"; +import type { MapPoolObject, ReadonlyMapPoolObject } from "./types"; -export function mapPoolToSerializedString(mapPool: MapPool): string { +export function mapPoolToSerializedString( + mapPool: ReadonlyMapPoolObject +): string { const serializedModes = []; for (const mode of modesShort) { @@ -15,7 +17,7 @@ export function mapPoolToSerializedString(mapPool: MapPool): string { return serializedModes.join(";").toLowerCase(); } -function stageIdsToBinary(input: StageId[]) { +function stageIdsToBinary(input: readonly StageId[]) { let result = "1"; for (const stageId of stageIds) { @@ -33,8 +35,10 @@ function binaryToHex(binary: string) { return parseInt(binary, 2).toString(16); } -export function serializedStringToMapPool(serialized: string) { - const result: MapPool = { +export function serializedStringToMapPool( + serialized: string +): ReadonlyMapPoolObject { + const result: MapPoolObject = { SZ: [], CB: [], RM: [], @@ -58,7 +62,7 @@ export function serializedStringToMapPool(serialized: string) { return result; } -function binaryToStageIds(binary: string): StageId[] { +function binaryToStageIds(binary: string): readonly StageId[] { const result: StageId[] = []; // first 1 is padding diff --git a/app/modules/map-pool-serializer/types.ts b/app/modules/map-pool-serializer/types.ts index c3e416dc0..a67d67034 100644 --- a/app/modules/map-pool-serializer/types.ts +++ b/app/modules/map-pool-serializer/types.ts @@ -1,4 +1,7 @@ import type { ModeShort } from "../in-game-lists"; import type { StageId } from "../in-game-lists"; -export type MapPool = Record; +export type MapPoolObject = Record; +export type ReadonlyMapPoolObject = Readonly< + Record +>; diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index 4e2b27a66..bc9db4ffe 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -41,6 +41,7 @@ import { import { actualNumber, id } from "~/utils/zod"; import { MapPoolSelector } from "~/components/MapPoolSelector"; import { Tags } from "../components/Tags"; +import { MapPool } from "~/modules/map-pool-serializer"; export const links: LinksFunction = () => { return [ @@ -244,7 +245,7 @@ function MapPoolInfo() { return (
- + { const deserializedMaps = (() => { if (!data.pool) return; - const mapPool = serializedStringToMapPool(data.pool); - return Object.entries(mapPool).flatMap(([mode, stages]) => - stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId })) - ); + return MapPool.toDbList(data.pool); })(); if (data.eventToEditId) { @@ -526,22 +519,22 @@ function BadgesAdder() { ); } -const DEFAULT_MAP_POOL = { +const DEFAULT_MAP_POOL = new MapPool({ SZ: [], TC: [], CB: [], RM: [], TW: [], -}; +}); function MapPoolSection() { const { t } = useTranslation(["game-misc", "calendar"]); - const data = useLoaderData(); + const { eventToEdit } = useLoaderData(); const [mapPool, setMapPool] = React.useState( - data.eventToEdit?.mapPool ?? DEFAULT_MAP_POOL + eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : DEFAULT_MAP_POOL ); const [includeMapPool, setIncludeMapPool] = React.useState( - Boolean(data.eventToEdit?.mapPool) + Boolean(eventToEdit?.mapPool) ); const handleMapPoolChange = ({ @@ -551,15 +544,17 @@ function MapPoolSection() { mode: ModeShort; stageId: StageId; }) => { - const newMapPool = mapPool[mode].includes(stageId) - ? { - ...mapPool, - [mode]: mapPool[mode].filter((id) => id !== stageId), - } - : { - ...mapPool, - [mode]: [...mapPool[mode], stageId], - }; + const newMapPool = new MapPool( + mapPool.parsed[mode].includes(stageId) + ? { + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + } + : { + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + } + ); setMapPool(newMapPool); }; @@ -567,11 +562,7 @@ function MapPoolSection() { return (
{includeMapPool && ( - + )}
diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 2cf364247..61eb662cc 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -28,11 +28,7 @@ import { mapPoolToNonEmptyModes, modesOrder, } from "~/modules/map-list-generator"; -import { - mapPoolToSerializedString, - serializedStringToMapPool, -} from "~/modules/map-pool-serializer"; -import type { MapPool } from "~/modules/map-pool-serializer/types"; +import { MapPool } from "~/modules/map-pool-serializer"; import styles from "~/styles/maps.css"; import { makeTitle } from "~/utils/strings"; import { calendarEventPage, ipLabsMaps } from "~/utils/urls"; @@ -85,13 +81,13 @@ export const loader = async ({ request }: LoaderArgs) => { }; }; -const DEFAULT_MAP_POOL = { +const DEFAULT_MAP_POOL = new MapPool({ SZ: [...stageIds], TC: [...stageIds], CB: [...stageIds], RM: [...stageIds], TW: [], -}; +}); export default function MapListPage() { const { t } = useTranslation(["common"]); @@ -114,7 +110,7 @@ export default function MapListPage() { handleMapPoolChange={handleMapPoolChange} /> { if (searchParams.has("pool")) { - return serializedStringToMapPool(searchParams.get("pool")!); + return new MapPool(searchParams.get("pool")!); } if (data?.mapPool) { - return data.mapPool; + return new MapPool(data.mapPool); } return DEFAULT_MAP_POOL; @@ -149,19 +145,21 @@ function useSearchParamMapPool() { mode: ModeShort; stageId: StageId; }) => { - const newMapPool = mapPool[mode].includes(stageId) - ? { - ...mapPool, - [mode]: mapPool[mode].filter((id) => id !== stageId), - } - : { - ...mapPool, - [mode]: [...mapPool[mode], stageId], - }; + const newMapPool = new MapPool( + mapPool.parsed[mode].includes(stageId) + ? { + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + } + : { + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + } + ); setSearchParams( { - pool: mapPoolToSerializedString(newMapPool), + pool: newMapPool.serialized, }, { replace: true, state: { scroll: false } } ); From edd3a1d91b35fc7768c9a7e1e03c77b30c1bc8c4 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Tue, 25 Oct 2022 22:54:58 +0200 Subject: [PATCH 3/7] Improve MapPoolSelector UI --- app/components/MapPoolSelector.tsx | 160 ++++++++++++++++++-- app/components/icons/ArrowLongLeft.tsx | 18 +++ app/hooks/useOnce.ts | 10 ++ app/modules/in-game-lists/modes.ts | 25 +-- app/modules/map-pool-serializer/map-pool.ts | 22 ++- app/routes/calendar/$id/index.tsx | 8 +- app/routes/calendar/new.tsx | 70 +++------ app/routes/maps.tsx | 119 ++++++++------- app/styles/calendar-new.css | 2 +- app/styles/maps.css | 40 +++-- app/styles/utils.css | 4 + app/styles/vars.css | 5 + app/utils/urls.ts | 10 +- public/locales/de/common.json | 2 + public/locales/de/game-misc.json | 7 +- public/locales/en/common.json | 2 + public/locales/en/game-misc.json | 7 +- 17 files changed, 355 insertions(+), 156 deletions(-) create mode 100644 app/components/icons/ArrowLongLeft.tsx create mode 100644 app/hooks/useOnce.ts diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 6896fd97f..a4a495691 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -3,23 +3,73 @@ import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; import type { ModeShort, StageId } from "~/modules/in-game-lists"; import { modes, stageIds } from "~/modules/in-game-lists"; -import { type MapPool } from "~/modules/map-pool-serializer"; +import { MapPool } from "~/modules/map-pool-serializer"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; +import { Button } from "~/components/Button"; +import { CrossIcon } from "./icons/Cross"; +import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; +import * as React from "react"; + +export type MapPoolSelectorProps = { + mapPool: MapPool; + handleRemoval?: () => void; + handleMapPoolChange: (mapPool: MapPool) => void; + className?: string; +}; export function MapPoolSelector({ mapPool, handleMapPoolChange, -}: { + handleRemoval, + className, +}: MapPoolSelectorProps) { + const { t } = useTranslation(); + + const handleStageModesChange = (newMapPool: MapPool) => { + handleMapPoolChange(newMapPool); + }; + + const handleClear = () => { + handleMapPoolChange(MapPool.EMPTY); + }; + + return ( +
+ {t("maps.mapPool")} +
+
+ {handleRemoval && ( + + )} + +
+ +
+
+ ); +} + +export type MapPoolStagesProps = { mapPool: MapPool; - handleMapPoolChange?: ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => void; -}) { - const { t } = useTranslation(["game-misc"]); + handleMapPoolChange?: (newMapPool: MapPool) => void; +}; + +export function MapPoolStages({ + mapPool, + handleMapPoolChange, +}: MapPoolStagesProps) { + const { t } = useTranslation(["game-misc", "common"]); const isPresentational = !handleMapPoolChange; @@ -29,6 +79,52 @@ export function MapPoolSelector({ return mapPool.hasStage(stageId); }; + const handleModeChange = ({ + mode, + stageId, + }: { + mode: ModeShort; + stageId: StageId; + }) => { + const newMapPool = mapPool.parsed[mode].includes(stageId) + ? new MapPool({ + ...mapPool.parsed, + [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), + }) + : new MapPool({ + ...mapPool.parsed, + [mode]: [...mapPool.parsed[mode], stageId], + }); + + handleMapPoolChange?.(newMapPool); + }; + + const handleStageClear = (stageId: StageId) => { + const newMapPool = new MapPool({ + TW: mapPool.parsed.TW.filter((id) => id !== stageId), + SZ: mapPool.parsed.SZ.filter((id) => id !== stageId), + TC: mapPool.parsed.TC.filter((id) => id !== stageId), + RM: mapPool.parsed.RM.filter((id) => id !== stageId), + CB: mapPool.parsed.CB.filter((id) => id !== stageId), + }); + + handleMapPoolChange?.(newMapPool); + }; + + const handleStageAdd = (stageId: StageId) => { + const newMapPool = new MapPool({ + TW: [...mapPool.parsed.TW, stageId], + SZ: [...mapPool.parsed.SZ, stageId], + TC: [...mapPool.parsed.TC, stageId], + RM: [...mapPool.parsed.RM, stageId], + CB: [...mapPool.parsed.CB, stageId], + }); + + handleMapPoolChange?.(newMapPool); + }; + + const id = React.useId(); + return (
{stageIds.filter(stageRowIsVisible).map((stageId) => ( @@ -40,8 +136,14 @@ export function MapPoolSelector({ width={80} height={45} /> -
-
{t(`game-misc:STAGE_${stageId}`)}
+
+
+ {t(`game-misc:STAGE_${stageId}`)} +
{modes.map((mode) => { const selected = mapPool.parsed[mode.short].includes(stageId); @@ -54,7 +156,8 @@ export function MapPoolSelector({ className={clsx("maps__mode", { selected, })} - alt={mode.long} + title={t(`game-misc:MODE_LONG_${mode.short}`)} + alt={t(`game-misc:MODE_LONG_${mode.short}`)} path={modeImageUrl(mode.short)} width={33} height={33} @@ -69,15 +172,18 @@ export function MapPoolSelector({ selected, })} onClick={() => - handleMapPoolChange?.({ mode: mode.short, stageId }) + handleModeChange?.({ mode: mode.short, stageId }) } type="button" + title={t(`game-misc:MODE_LONG_${mode.short}`)} + aria-describedby={`${id}-stage-name-${stageId}`} + aria-pressed={selected} > {mode.long} ); })} + {!isPresentational && + (mapPool.hasStage(stageId) ? ( +
diff --git a/app/components/icons/ArrowLongLeft.tsx b/app/components/icons/ArrowLongLeft.tsx new file mode 100644 index 000000000..b419e99ea --- /dev/null +++ b/app/components/icons/ArrowLongLeft.tsx @@ -0,0 +1,18 @@ +export function ArrowLongLeftIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/hooks/useOnce.ts b/app/hooks/useOnce.ts new file mode 100644 index 000000000..4296bf40a --- /dev/null +++ b/app/hooks/useOnce.ts @@ -0,0 +1,10 @@ +import { useMemo } from "react"; + +/** + * Utility hook for calling `useMemo(f, [])`, when you're sure it needs no + * revalidation but feel bad for getting shamed by eslint everytime :D + */ +export function useOnce(factory: () => T) { + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(factory, []); +} diff --git a/app/modules/in-game-lists/modes.ts b/app/modules/in-game-lists/modes.ts index 62736c388..d44939f0f 100644 --- a/app/modules/in-game-lists/modes.ts +++ b/app/modules/in-game-lists/modes.ts @@ -1,24 +1,9 @@ export const modes = [ - { - short: "TW", - long: "Turf War", - }, - { - short: "SZ", - long: "Splat Zones", - }, - { - short: "TC", - long: "Tower Control", - }, - { - short: "RM", - long: "Rainmaker", - }, - { - short: "CB", - long: "Clam Blitz", - }, + { short: "TW" }, + { short: "SZ" }, + { short: "TC" }, + { short: "RM" }, + { short: "CB" }, ] as const; export const modesShort = modes.map((mode) => mode.short); diff --git a/app/modules/map-pool-serializer/map-pool.ts b/app/modules/map-pool-serializer/map-pool.ts index 8c26c654b..8639d5416 100644 --- a/app/modules/map-pool-serializer/map-pool.ts +++ b/app/modules/map-pool-serializer/map-pool.ts @@ -6,7 +6,11 @@ import type { ReadonlyMapPoolObject, MapPoolObject } from "./types"; import clone from "just-clone"; import type { MapPoolMap } from "~/db/types"; import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator"; -import type { ModeShort, StageId } from "~/modules/in-game-lists"; +import { + type ModeShort, + type StageId, + stageIds, +} from "~/modules/in-game-lists"; type DbMapPoolList = Array>; @@ -84,4 +88,20 @@ export class MapPool { toJSON() { return this.parsed; } + + static EMPTY = new MapPool({ + SZ: [], + TC: [], + CB: [], + RM: [], + TW: [], + }); + + static ANARCHY = new MapPool({ + SZ: [...stageIds], + TC: [...stageIds], + CB: [...stageIds], + RM: [...stageIds], + TW: [], + }); } diff --git a/app/routes/calendar/$id/index.tsx b/app/routes/calendar/$id/index.tsx index bc9db4ffe..28b2b8c69 100644 --- a/app/routes/calendar/$id/index.tsx +++ b/app/routes/calendar/$id/index.tsx @@ -33,13 +33,13 @@ import { discordFullName, makeTitle } from "~/utils/strings"; import { calendarEditPage, calendarReportWinnersPage, - mapsPage, navIconUrl, + readonlyMapsPage, resolveBaseUrl, userPage, } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; -import { MapPoolSelector } from "~/components/MapPoolSelector"; +import { MapPoolStages } from "~/components/MapPoolSelector"; import { Tags } from "../components/Tags"; import { MapPool } from "~/modules/map-pool-serializer"; @@ -245,10 +245,10 @@ function MapPoolInfo() { return (
- + diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index b19e1076d..5357ffadb 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -20,14 +20,12 @@ import { TrashIcon } from "~/components/icons/Trash"; import { Input } from "~/components/Input"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; -import { Toggle } from "~/components/Toggle"; import { CALENDAR_EVENT } from "~/constants"; import { db } from "~/db"; import type { Badge as BadgeType, CalendarEventTag } from "~/db/types"; import { useIsMounted } from "~/hooks/useIsMounted"; import { requireUser } from "~/modules/auth"; import { i18next } from "~/modules/i18n"; -import type { ModeShort, StageId } from "~/modules/in-game-lists"; import { MapPool } from "~/modules/map-pool-serializer"; import { canEditCalendarEvent } from "~/permissions"; import calendarNewStyles from "~/styles/calendar-new.css"; @@ -519,61 +517,41 @@ function BadgesAdder() { ); } -const DEFAULT_MAP_POOL = new MapPool({ - SZ: [], - TC: [], - CB: [], - RM: [], - TW: [], -}); function MapPoolSection() { - const { t } = useTranslation(["game-misc", "calendar"]); + const { t } = useTranslation(["game-misc", "common"]); const { eventToEdit } = useLoaderData(); const [mapPool, setMapPool] = React.useState( - eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : DEFAULT_MAP_POOL + eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : MapPool.EMPTY ); const [includeMapPool, setIncludeMapPool] = React.useState( Boolean(eventToEdit?.mapPool) ); - const handleMapPoolChange = ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => { - const newMapPool = new MapPool( - mapPool.parsed[mode].includes(stageId) - ? { - ...mapPool.parsed, - [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), - } - : { - ...mapPool.parsed, - [mode]: [...mapPool.parsed[mode], stageId], - } - ); + const id = React.useId(); - setMapPool(newMapPool); - }; + return includeMapPool ? ( + <> + - return ( -
- {includeMapPool && ( - - )} - -
- - {includeMapPool && ( - - )} -
+ setIncludeMapPool(false)} + handleMapPoolChange={setMapPool} + /> + + ) : ( +
+ +
); } diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 61eb662cc..1c63249ea 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -17,12 +17,7 @@ import { Main } from "~/components/Main"; import { Toggle } from "~/components/Toggle"; import { db } from "~/db"; import { i18next } from "~/modules/i18n"; -import { - stageIds, - type ModeShort, - type ModeWithStage, - type StageId, -} from "~/modules/in-game-lists"; +import { stageIds, type ModeWithStage } from "~/modules/in-game-lists"; import { generateMapList, mapPoolToNonEmptyModes, @@ -33,11 +28,18 @@ import styles from "~/styles/maps.css"; import { makeTitle } from "~/utils/strings"; import { calendarEventPage, ipLabsMaps } from "~/utils/urls"; import { type SendouRouteHandle } from "~/utils/remix"; -import { MapPoolSelector } from "~/components/MapPoolSelector"; +import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector"; +import { EditIcon } from "~/components/icons/Edit"; +import { useOnce } from "~/hooks/useOnce"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; -export const unstable_shouldReload: ShouldReloadFunction = () => false; +export const unstable_shouldReload: ShouldReloadFunction = ({ url }) => { + const searchParams = new URL(url).searchParams; + // Only let loader reload data if we're not currently editing the map pool + // and persisting it in the search params. + return searchParams.has("readonly"); +}; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -81,34 +83,43 @@ export const loader = async ({ request }: LoaderArgs) => { }; }; -const DEFAULT_MAP_POOL = new MapPool({ - SZ: [...stageIds], - TC: [...stageIds], - CB: [...stageIds], - RM: [...stageIds], - TW: [], -}); - export default function MapListPage() { const { t } = useTranslation(["common"]); const data = useLoaderData(); const [searchParams] = useSearchParams(); - const { mapPool, handleMapPoolChange } = useSearchParamMapPool(); + const { mapPool, handleMapPoolChange, readonly, switchToEditMode } = + useSearchParamPersistedMapPool(); return (
- {data.calendarEvent && !searchParams.has("pool") && ( -
- {t("common:maps.mapPool")}:{" "} - - {data.calendarEvent.name} - + {searchParams.has("readonly") && data.calendarEvent && ( +
+
+ {t("common:maps.mapPool")}:{" "} + { + + {data.calendarEvent.name} + + } +
+
)} - + {readonly ? ( + + ) : ( + + )}
(); const [searchParams, setSearchParams] = useSearchParams(); - const mapPool = (() => { + const initialMapPool = useOnce(() => { if (searchParams.has("pool")) { return new MapPool(searchParams.get("pool")!); } - if (data?.mapPool) { + if (data.mapPool) { return new MapPool(data.mapPool); } - return DEFAULT_MAP_POOL; - })(); + return MapPool.ANARCHY; + }); - const handleMapPoolChange = ({ - mode, - stageId, - }: { - mode: ModeShort; - stageId: StageId; - }) => { - const newMapPool = new MapPool( - mapPool.parsed[mode].includes(stageId) - ? { - ...mapPool.parsed, - [mode]: mapPool.parsed[mode].filter((id) => id !== stageId), - } - : { - ...mapPool.parsed, - [mode]: [...mapPool.parsed[mode], stageId], - } - ); + const [mapPool, setMapPool] = React.useState(initialMapPool); + const handleMapPoolChange = (newMapPool: MapPool) => { + setMapPool(newMapPool); setSearchParams( { pool: newMapPool.serialized, @@ -165,9 +161,20 @@ function useSearchParamMapPool() { ); }; + const switchToEditMode = () => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.delete("readonly"); + setSearchParams(newSearchParams, { + replace: false, + state: { scroll: false }, + }); + }; + return { mapPool, + readonly: searchParams.has("readonly"), handleMapPoolChange, + switchToEditMode, }; } @@ -192,13 +199,16 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) { setMapList(list); }; + const disabled = + mapPool.isEmpty() || (szEveryOther && !mapPool.hasMode("SZ")); + return (
- {mapList && ( @@ -206,7 +216,12 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
    {mapList.map(({ mode, stageId }, i) => (
  1. - {t(`game-misc:MODE_SHORT_${mode}`)}{" "} + + {t(`game-misc:MODE_SHORT_${mode}`)} + {" "} {t(`game-misc:STAGE_${stageId}`)}
  2. ))} diff --git a/app/styles/calendar-new.css b/app/styles/calendar-new.css index 291f6adc1..fdf61d77f 100644 --- a/app/styles/calendar-new.css +++ b/app/styles/calendar-new.css @@ -1,5 +1,5 @@ .calendar-new__container { - max-width: 32rem; + max-width: 38rem; } .calendar-new__select { diff --git a/app/styles/maps.css b/app/styles/maps.css index cc680c1d4..5d064d305 100644 --- a/app/styles/maps.css +++ b/app/styles/maps.css @@ -1,9 +1,19 @@ .maps__container { - max-width: 32rem; + max-width: 38rem; +} + +.maps__pool-meta { + display: flex; + align-items: center; + justify-content: space-between; } .maps__pool-info { - font-size: var(--fonts-xxs); + font-size: var(--fonts-xs); + font-weight: var(--bold); +} + +.maps__pool-info a { font-weight: var(--semi-bold); } @@ -49,29 +59,27 @@ .maps__mode-button { padding: 0; - padding: var(--s-1-5); - border: none; - border-radius: var(--rounded); - background-color: var(--bg-darker); + padding: var(--s-1); + border: 2px solid var(--bg-darker); + border-radius: var(--rounded-full); + background-color: transparent; color: var(--theme); opacity: 1 !important; outline: initial; } .maps__mode-button.selected { - background-color: var(--theme-very-transparent); + border: 2px solid transparent; + background-color: var(--bg-mode-active); } .maps__stage-image { border-radius: var(--rounded); } -.maps__mode { - filter: grayscale(100%); -} - -.maps__mode.selected { - filter: unset; +.maps__mode:not(.selected) { + filter: var(--inactive-image-filter); + opacity: 0.6; } .maps__map-list-creator { @@ -96,3 +104,9 @@ font-weight: var(--semi-bold); margin-block-start: var(--s-4); } + +.maps__mode-abbr { + color: var(--text-lighter); + font-weight: var(--bold); + text-decoration: none; +} diff --git a/app/styles/utils.css b/app/styles/utils.css index fef6c85f5..f9b542e43 100644 --- a/app/styles/utils.css +++ b/app/styles/utils.css @@ -126,6 +126,10 @@ justify-content: center; } +.justify-end { + justify-content: flex-end; +} + .flex-wrap { flex-wrap: wrap; } diff --git a/app/styles/vars.css b/app/styles/vars.css index 71e346065..c626fc365 100644 --- a/app/styles/vars.css +++ b/app/styles/vars.css @@ -7,6 +7,7 @@ html { --bg-darker-transparent: hsla(202deg 90% 90% / 65%); --bg-ability: rgb(3 6 7); --bg-badge: #000; + --bg-mode-active: hsl(255deg 66.7% 50% / 40%); --abilities-button-bg: hsl(237deg 32% 30%); --badge-text: rgb(255 255 255 / 95%); --border: hsl(237deg 100% 86%); @@ -31,6 +32,7 @@ html { --theme-semi-transparent-vibrant: hsl(255deg 100% 81% / 75%); --theme-secondary: hsl(85deg 66.7% 55.3%); --rounded: 16px; + --rounded-full: 200px; --rounded-sm: 10px; --fonts-xl: 1.5rem; --fonts-lg: 1.2rem; @@ -73,6 +75,7 @@ html { --s-96: 2rem; --sparse: 0.4px; --label-margin: var(--s-1); + --inactive-image-filter: grayscale(100%) brightness(30%); } html.dark { @@ -84,6 +87,7 @@ html.dark { --bg-darker-transparent: hsla(237.3deg 42.3% 26.6% / 90%); --bg-ability: rgb(17 19 43); --bg-badge: #000; + --bg-mode-active: var(--theme-transparent); --abilities-button-bg: hsl(237.3deg 42.3% 26.6%); --border: hsl(237.3deg 42.3% 45.6%); --button-text: rgb(0 0 0 / 85%); @@ -104,6 +108,7 @@ html.dark { --theme-transparent-vibrant: hsl(255deg 78% 65% / 54%); --theme-semi-transparent-vibrant: hsl(255deg 78% 65% / 75%); --theme-secondary: hsl(85deg 66.7% 55.3%); + --inactive-image-filter: grayscale(100%) brightness(130%); } html.dark .light-mode-only { diff --git a/app/utils/urls.ts b/app/utils/urls.ts index bef1ce4dd..bb77c5894 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -1,5 +1,11 @@ import slugify from "slugify"; -import type { Badge, GearType, MapPoolMap, User } from "~/db/types"; +import type { + Badge, + CalendarEvent, + GearType, + MapPoolMap, + User, +} from "~/db/types"; import type { ModeShort, weaponCategories } from "~/modules/in-game-lists"; import type { Ability, @@ -74,6 +80,8 @@ export const calendarReportWinnersPage = (eventId: number) => `/calendar/${eventId}/report-winners`; export const mapsPage = (eventId?: MapPoolMap["calendarEventId"]) => `/maps${eventId ? `?eventId=${eventId}` : ""}`; +export const readonlyMapsPage = (eventId: CalendarEvent["id"]) => + `/maps?readonly&eventId=${eventId}`; export const articlePage = (slug: string) => `/a/${slug}`; export const analyzerPage = (args?: { weaponId: MainWeaponId; diff --git a/public/locales/de/common.json b/public/locales/de/common.json index af292c7b5..7820cc344 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -34,6 +34,8 @@ "actions.delete": "Löschen", "actions.loadMore": "Mehr laden", "actions.close": "Schließen", + "actions.clear": "Leeren", + "actions.selectAll": "Alle auswählen", "maps.createMapList": "Arenen-Liste erstellen", "maps.halfSz": "50% Herrschaft", diff --git a/public/locales/de/game-misc.json b/public/locales/de/game-misc.json index 4e3c81ce7..d9e393f1c 100644 --- a/public/locales/de/game-misc.json +++ b/public/locales/de/game-misc.json @@ -15,5 +15,10 @@ "MODE_SHORT_SZ": "HS", "MODE_SHORT_TC": "TK", "MODE_SHORT_RM": "OG", - "MODE_SHORT_CB": "MC" + "MODE_SHORT_CB": "MC", + "MODE_LONG_TW": "Revierkampf", + "MODE_LONG_SZ": "Herrschaft", + "MODE_LONG_TC": "Turmkommando", + "MODE_LONG_RM": "Operation Goldfisch", + "MODE_LONG_CB": "Muschelchaos" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index db52a437d..c9f7a376e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -36,6 +36,8 @@ "actions.loadMore": "Load more", "actions.copyToClipboard": "Copy to clipboard", "actions.close": "Close", + "actions.clear": "Clear", + "actions.selectAll": "Select All", "maps.createMapList": "Create map list", "maps.halfSz": "50% SZ", diff --git a/public/locales/en/game-misc.json b/public/locales/en/game-misc.json index be8229456..780d33e38 100644 --- a/public/locales/en/game-misc.json +++ b/public/locales/en/game-misc.json @@ -15,5 +15,10 @@ "MODE_SHORT_SZ": "SZ", "MODE_SHORT_TC": "TC", "MODE_SHORT_RM": "RM", - "MODE_SHORT_CB": "CB" + "MODE_SHORT_CB": "CB", + "MODE_LONG_TW": "Turf War", + "MODE_LONG_SZ": "Splat Zones", + "MODE_LONG_TC": "Tower Control", + "MODE_LONG_RM": "Rainmaker", + "MODE_LONG_CB": "Clam Blitz" } From 63154e507e336330ce6e7b8591204775ef61bfe5 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 01:05:01 +0200 Subject: [PATCH 4/7] Initial support for map pool templates --- app/components/MapPoolSelector.tsx | 114 +++++++++++++++++--- app/modules/map-pool-serializer/map-pool.ts | 29 +++++ app/styles/common.css | 3 +- app/styles/maps.css | 12 +++ app/utils/strings.ts | 27 +++++ public/locales/de/common.json | 6 ++ public/locales/en/common.json | 6 ++ 7 files changed, 182 insertions(+), 15 deletions(-) diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index a4a495691..1bc2ea46a 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -1,11 +1,16 @@ import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; -import type { ModeShort, StageId } from "~/modules/in-game-lists"; +import { + type ModeShort, + modesShort, + type StageId, +} from "~/modules/in-game-lists"; import { modes, stageIds } from "~/modules/in-game-lists"; import { MapPool } from "~/modules/map-pool-serializer"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; import { Button } from "~/components/Button"; +import { split, startsWith } from "~/utils/strings"; import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; @@ -25,31 +30,57 @@ export function MapPoolSelector({ }: MapPoolSelectorProps) { const { t } = useTranslation(); + const [template, setTemplate] = React.useState( + detectTemplate(mapPool) + ); + const handleStageModesChange = (newMapPool: MapPool) => { + setTemplate(detectTemplate(newMapPool)); handleMapPoolChange(newMapPool); }; const handleClear = () => { + setTemplate("none"); handleMapPoolChange(MapPool.EMPTY); }; + const handleTemplateChange = (template: MapPoolTemplateValue) => { + setTemplate(template); + if (template === "none") { + return; + } + + if (startsWith(template, "preset:")) { + const [, presetId] = split(template, ":"); + + handleMapPoolChange(MapPool[presetId]); + return; + } + }; + return (
    {t("maps.mapPool")} -
    -
    - {handleRemoval && ( - - )} - + )} + +
    +
    +
    +
    ); } + +type MapModePresetId = "ANARCHY" | "ALL" | ModeShort; + +const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort]; + +type MapPoolTemplateValue = "none" | `preset:${MapModePresetId}`; + +function detectTemplate(mapPool: MapPool): MapPoolTemplateValue { + for (const presetId of presetIds) { + if (MapPool[presetId].serialized === mapPool.serialized) { + return `preset:${presetId}`; + } + } + return "none"; +} + +type MapPoolTemplateSelectProps = { + value: MapPoolTemplateValue; + handleChange: (newValue: MapPoolTemplateValue) => void; +}; + +function MapPoolTemplateSelect({ + handleChange, + value, +}: MapPoolTemplateSelectProps) { + const { t } = useTranslation(["game-misc", "common"]); + + return ( + + ); +} diff --git a/app/modules/map-pool-serializer/map-pool.ts b/app/modules/map-pool-serializer/map-pool.ts index 8639d5416..e2c3e246e 100644 --- a/app/modules/map-pool-serializer/map-pool.ts +++ b/app/modules/map-pool-serializer/map-pool.ts @@ -97,6 +97,14 @@ export class MapPool { TW: [], }); + static ALL = new MapPool({ + SZ: [...stageIds], + TC: [...stageIds], + CB: [...stageIds], + RM: [...stageIds], + TW: [...stageIds], + }); + static ANARCHY = new MapPool({ SZ: [...stageIds], TC: [...stageIds], @@ -104,4 +112,25 @@ export class MapPool { RM: [...stageIds], TW: [], }); + + static SZ = new MapPool({ + ...MapPool.EMPTY.parsed, + SZ: [...stageIds], + }); + static TC = new MapPool({ + ...MapPool.EMPTY.parsed, + TC: [...stageIds], + }); + static CB = new MapPool({ + ...MapPool.EMPTY.parsed, + CB: [...stageIds], + }); + static RM = new MapPool({ + ...MapPool.EMPTY.parsed, + RM: [...stageIds], + }); + static TW = new MapPool({ + ...MapPool.EMPTY.parsed, + TW: [...stageIds], + }); } diff --git a/app/styles/common.css b/app/styles/common.css index 2aa9a71f0..cb7404af8 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -243,7 +243,8 @@ article { select { all: unset; - width: 90%; + width: 100%; + box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--rounded); background: var(--select-background, var(--bg-lighter)); diff --git a/app/styles/maps.css b/app/styles/maps.css index 5d064d305..4c0ad79c2 100644 --- a/app/styles/maps.css +++ b/app/styles/maps.css @@ -110,3 +110,15 @@ font-weight: var(--bold); text-decoration: none; } + +.maps__template-selection { + display: grid; + gap: var(--s-2); + grid-template-columns: 1fr; +} + +@media screen and (min-width: 640px) { + .maps__template-selection { + grid-template-columns: 1fr 1fr; + } +} diff --git a/app/utils/strings.ts b/app/utils/strings.ts index 7b3202a81..ebe7532c2 100644 --- a/app/utils/strings.ts +++ b/app/utils/strings.ts @@ -30,3 +30,30 @@ export function semiRandomId() { export const rawSensToString = (sens: number) => `${sens > 0 ? "+" : ""}${sens / 10}`; + +type WithStart< + S extends string, + Start extends string +> = S extends `${Start}${infer Rest}` ? `${Start}${Rest}` : never; + +export function startsWith( + str: S, + start: Start +): str is WithStart { + return str.startsWith(start); +} + +type Split = string extends S + ? string[] + : S extends "" + ? [] + : S extends `${infer T}${Sep}${infer U}` + ? [T, ...Split] + : [S]; + +export function split( + str: S, + seperator: Sep +) { + return str.split(seperator) as Split; +} diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 7820cc344..bff10d8fb 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -41,6 +41,12 @@ "maps.halfSz": "50% Herrschaft", "maps.mapPool": "Arenen-Pool", "maps.tournamentMaplist": "Arenen-Liste für Turnier erstellen (maps.iplabs.ink)", + "maps.template": "Vorlage", + "maps.template.none": "Keine", + "maps.template.presets": "Voreinstellungen", + "maps.template.preset.ANARCHY": "Anarchie-Modi", + "maps.template.preset.ALL": "Alle Modi", + "maps.template.preset.onlyMode": "Nur {{modeName}}", "results": "Ergebnisse", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c9f7a376e..c69d6a4a5 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -43,6 +43,12 @@ "maps.halfSz": "50% SZ", "maps.mapPool": "Map pool", "maps.tournamentMaplist": "Create tournament map list (maps.iplabs.ink)", + "maps.template": "Template", + "maps.template.none": "None", + "maps.template.presets": "Presets", + "maps.template.preset.ANARCHY": "Anarchy Modes", + "maps.template.preset.ALL": "All Modes", + "maps.template.preset.onlyMode": "Only {{modeName}}", "results": "Results", From 0a85ad08d2f9baa1d69df5966da05b33f5c4673e Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 01:18:32 +0200 Subject: [PATCH 5/7] Add recent events to map pool templates --- app/components/MapPoolSelector.tsx | 38 ++++++++++++++++++- .../calendar/findRecentMapPoolsByAuthorId.sql | 21 ++++++++++ app/db/models/calendar/queries.server.ts | 20 ++++++++++ app/routes/calendar/new.tsx | 7 +++- app/routes/maps.tsx | 20 ++++++++-- public/locales/de/common.json | 1 + public/locales/en/common.json | 1 + 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 app/db/models/calendar/findRecentMapPoolsByAuthorId.sql diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 1bc2ea46a..9fe4d955f 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -14,12 +14,19 @@ import { split, startsWith } from "~/utils/strings"; import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; +import type { CalendarEvent } from "~/db/types"; export type MapPoolSelectorProps = { mapPool: MapPool; handleRemoval?: () => void; - handleMapPoolChange: (mapPool: MapPool) => void; + handleMapPoolChange: ( + mapPool: MapPool, + event?: Pick + ) => void; className?: string; + recentEvents?: Array< + Pick & { serializedMapPool: string } + >; }; export function MapPoolSelector({ @@ -27,6 +34,7 @@ export function MapPoolSelector({ handleMapPoolChange, handleRemoval, className, + recentEvents, }: MapPoolSelectorProps) { const { t } = useTranslation(); @@ -56,6 +64,17 @@ export function MapPoolSelector({ handleMapPoolChange(MapPool[presetId]); 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; + } }; return ( @@ -80,6 +99,7 @@ export function MapPoolSelector({
    void; + recentEvents?: Pick[]; }; function MapPoolTemplateSelect({ handleChange, value, + recentEvents, }: MapPoolTemplateSelectProps) { const { t } = useTranslation(["game-misc", "common"]); @@ -302,6 +327,15 @@ function MapPoolTemplateSelect({ ))} + {recentEvents && recentEvents.length > 0 && ( + + {recentEvents.map((event) => ( + + ))} + + )} ); diff --git a/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql b/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql new file mode 100644 index 000000000..6ef3c3ba8 --- /dev/null +++ b/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql @@ -0,0 +1,21 @@ +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" +where + "CalendarEvent"."authorId" = @authorId +group by + "CalendarEvent"."id" +order by + "CalendarEvent"."id" desc +limit 5 \ No newline at end of file diff --git a/app/db/models/calendar/queries.server.ts b/app/db/models/calendar/queries.server.ts index 60ad44186..a8788112c 100644 --- a/app/db/models/calendar/queries.server.ts +++ b/app/db/models/calendar/queries.server.ts @@ -36,6 +36,7 @@ import upcomingEventsSql from "./upcomingEvents.sql"; import createMapPoolMapSql from "./createMapPoolMap.sql"; import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql"; import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql"; +import findRecentMapPoolsByAuthorIdSql from "./findRecentMapPoolsByAuthorId.sql"; const createStm = sql.prepare(createSql); const updateStm = sql.prepare(updateSql); @@ -467,3 +468,22 @@ export function eventsToReport(authorId?: CalendarEvent["authorId"]) { }) as Array> ).map((row) => ({ id: row.id, name: row.name })); } + +const findRecentMapPoolsByAuthorIdStm = sql.prepare( + findRecentMapPoolsByAuthorIdSql +); +export function findRecentMapPoolsByAuthorId( + authorId: CalendarEvent["authorId"] +) { + return ( + findRecentMapPoolsByAuthorIdStm.all({ authorId }) as Array< + Pick & { + mapPool: string; + } + > + ).map((row) => ({ + id: row.id, + name: row.name, + serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)), + })); +} diff --git a/app/routes/calendar/new.tsx b/app/routes/calendar/new.tsx index 5357ffadb..6d51ba6e5 100644 --- a/app/routes/calendar/new.tsx +++ b/app/routes/calendar/new.tsx @@ -192,6 +192,9 @@ export const loader = async ({ request }: LoaderArgs) => { return json({ managedBadges: db.badges.managedByUserId(user.id), + recentEventsWithMapPools: db.calendarEvents.findRecentMapPoolsByAuthorId( + user.id + ), eventToEdit: canEditEvent ? { ...eventToEdit, @@ -520,7 +523,8 @@ function BadgesAdder() { function MapPoolSection() { const { t } = useTranslation(["game-misc", "common"]); - const { eventToEdit } = useLoaderData(); + const { eventToEdit, recentEventsWithMapPools } = + useLoaderData(); const [mapPool, setMapPool] = React.useState( eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : MapPool.EMPTY ); @@ -539,6 +543,7 @@ function MapPoolSection() { mapPool={mapPool} handleRemoval={() => setIncludeMapPool(false)} handleMapPoolChange={setMapPool} + recentEvents={recentEventsWithMapPools} /> ) : ( diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 1c63249ea..950e01ee9 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -31,6 +31,8 @@ import { type SendouRouteHandle } from "~/utils/remix"; import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector"; import { EditIcon } from "~/components/icons/Edit"; import { useOnce } from "~/hooks/useOnce"; +import { getUser } from "~/modules/auth"; +import type { CalendarEvent } from "~/db/types"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; @@ -61,6 +63,7 @@ export const handle: SendouRouteHandle = { }; export const loader = async ({ request }: LoaderArgs) => { + const user = await getUser(request); const url = new URL(request.url); const calendarEventId = url.searchParams.get("eventId"); const t = await i18next.getFixedT(request); @@ -79,6 +82,9 @@ export const loader = async ({ request }: LoaderArgs) => { mapPool: event ? db.calendarEvents.findMapPoolByEventId(event.eventId) : null, + recentEventsWithMapPools: user + ? db.calendarEvents.findRecentMapPoolsByAuthorId(user.id) + : undefined, title: makeTitle([t("pages.maps")]), }; }; @@ -118,6 +124,7 @@ export default function MapListPage() { )}
    { + const handleMapPoolChange = ( + newMapPool: MapPool, + event?: Pick + ) => { setMapPool(newMapPool); setSearchParams( - { - pool: newMapPool.serialized, - }, + event + ? { eventId: event.id.toString() } + : { + pool: newMapPool.serialized, + }, { replace: true, state: { scroll: false } } ); }; diff --git a/public/locales/de/common.json b/public/locales/de/common.json index bff10d8fb..b7e97835e 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -44,6 +44,7 @@ "maps.template": "Vorlage", "maps.template.none": "Keine", "maps.template.presets": "Voreinstellungen", + "maps.template.yourRecentEvents": "Deine Events", "maps.template.preset.ANARCHY": "Anarchie-Modi", "maps.template.preset.ALL": "Alle Modi", "maps.template.preset.onlyMode": "Nur {{modeName}}", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index c69d6a4a5..127e88e7b 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -46,6 +46,7 @@ "maps.template": "Template", "maps.template.none": "None", "maps.template.presets": "Presets", + "maps.template.yourRecentEvents": "Recent Events", "maps.template.preset.ANARCHY": "Anarchy Modes", "maps.template.preset.ALL": "All Modes", "maps.template.preset.onlyMode": "Only {{modeName}}", From d1b13676511f4c84475525eb04492d4a5ed885a8 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:00:28 +0200 Subject: [PATCH 6/7] Add event search to map pool templates --- app/components/Combobox.tsx | 232 ++++++++++++------ app/components/MapPoolSelector.tsx | 69 +++++- .../calendar/findAllEventsWithMapPools.sql | 18 ++ app/db/models/calendar/queries.server.ts | 16 ++ app/hooks/swr.ts | 22 +- app/routes/calendar/map-pool-events.ts | 12 + app/routes/maps.tsx | 1 + app/styles/common.css | 12 + app/utils/urls.ts | 3 + public/locales/de/common.json | 6 + public/locales/en/common.json | 6 + 11 files changed, 315 insertions(+), 82 deletions(-) create mode 100644 app/db/models/calendar/findAllEventsWithMapPools.sql create mode 100644 app/routes/calendar/map-pool-events.ts diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx index 9e8027d6f..e5dacdfd1 100644 --- a/app/components/Combobox.tsx +++ b/app/components/Combobox.tsx @@ -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 { initialValue?: ComboboxOption; clearsInputOnFocus?: boolean; onChange?: (selectedOption?: ComboboxOption) => void; + fullWidth?: boolean; } export function Combobox>({ @@ -49,11 +51,16 @@ export function Combobox>({ className, id, isLoading = false, + fullWidth = false, }: ComboboxProps) { - const [selectedOption, setSelectedOption] = - React.useState>(); - const [lastSelectedOption, setLastSelectedOption] = - React.useState>(); + const { t } = useTranslation(); + + const [selectedOption, setSelectedOption] = React.useState< + Unpacked | undefined + >(initialValue); + const [lastSelectedOption, setLastSelectedOption] = React.useState< + Unpacked | undefined + >(initialValue); const [query, setQuery] = React.useState(""); React.useEffect(() => { @@ -75,73 +82,85 @@ export function Combobox>({ const noMatches = filteredOptions.length === 0; + const displayValue = (option: Unpacked) => { + return option?.label ?? ""; + }; + return ( - { - onChange?.(selected); - setSelectedOption(selected); - setLastSelectedOption(selected); - }} - name={inputName} - disabled={isLoading} - > - { - if (clearsInputOnFocus) { - setSelectedOption(undefined); - } +
    + { + 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)?.label ?? "" - } - data-cy={`${inputName}-combobox-input`} - id={id} - required={required} - /> - - {noMatches ? ( -
    - No matches found 🤔 -
    - ) : ( - filteredOptions.map((option) => ( - - {({ active }) => ( -
  3. - {option.imgPath && ( - - )} - {option.label} -
  4. - )} -
    - )) - )} -
    -
    + { + 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} + /> + + {isLoading ? ( +
    {t("actions.loading")}
    + ) : noMatches ? ( +
    + {t("forms.errors.noSearchMatches")}{" "} + 🤔 +
    + ) : ( + filteredOptions.map((option) => ( + + {({ active }) => ( +
  5. + {option.imgPath && ( + + )} + {option.label} +
  6. + )} +
    + )) + )} +
    + +
    ); } @@ -157,6 +176,7 @@ export function UserCombobox({ ComboboxProps>, "inputName" | "onChange" | "className" | "id" | "required" > & { userIdsToOmit?: Set; initialUserId?: number }) { + const { t } = useTranslation(); const { users, isLoading, isError } = useUsers(); const options = React.useMemo(() => { @@ -181,9 +201,7 @@ export function UserCombobox({ if (isError) { return ( -
    - Something went wrong. Try reloading the page. -
    +
    {t("errors.genericReload")}
    ); } @@ -191,7 +209,7 @@ export function UserCombobox({ ); } + +const mapPoolEventToOption = ( + e: SerializedMapPoolEvent +): ComboboxOption> => ({ + serializedMapPool: e.serializedMapPool, + label: e.name, + value: e.id.toString(), +}); + +type MapPoolEventsComboboxProps = Pick< + ComboboxProps>, + "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 ( +
    {t("errors.genericReload")}
    + ); + } + + return ( + { + onChange( + e && { + id: parseInt(e.value, 10), + name: e.label, + serializedMapPool: e.serializedMapPool, + } + ); + }} + className={className} + id={id} + required={required} + isLoading={isLoading} + fullWidth + /> + ); +} diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 9fe4d955f..d02d340a1 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -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 ) => void; className?: string; - recentEvents?: Array< - Pick & { serializedMapPool: string } - >; + recentEvents?: SerializedMapPoolEvent[]; + initialEvent?: Pick; }; export function MapPoolSelector({ @@ -35,11 +38,20 @@ export function MapPoolSelector({ handleRemoval, className, recentEvents, + initialEvent, }: MapPoolSelectorProps) { const { t } = useTranslation(); const [template, setTemplate] = React.useState( - 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(); }; return ( @@ -101,6 +116,12 @@ export function MapPoolSelector({ handleChange={handleTemplateChange} recentEvents={recentEvents} /> + {template === "event" && ( + + )}
    + {(["ANARCHY", "ALL"] as const).map((presetId) => ( Date: Wed, 26 Oct 2022 19:55:16 +0200 Subject: [PATCH 7/7] Update translation-progress.md --- translation-progress.md | 307 +++++++++++++++++++++++++++------------- 1 file changed, 205 insertions(+), 102 deletions(-) diff --git a/translation-progress.md b/translation-progress.md index aca22c713..f601d5717 100644 --- a/translation-progress.md +++ b/translation-progress.md @@ -39,12 +39,26 @@ ### 🟡 common.json -**60/61** +**60/75**
    Missing - pages.object-damage-calculator +- actions.loading +- actions.clear +- actions.selectAll +- actions.search +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    @@ -67,9 +81,20 @@ -### 🟢 game-misc.json +### 🟡 game-misc.json -**17/17** +**17/22** + +
    +Missing + +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB + +
    ### 🟢 user.json @@ -116,7 +141,7 @@ ### 🟡 common.json -**59/61** +**73/75**
    Missing @@ -147,7 +172,7 @@ ### 🟢 game-misc.json -**17/17** +**22/22** ### 🟢 user.json @@ -205,7 +230,7 @@ ### 🟡 common.json -**48/61** +**48/75**
    Missing @@ -219,10 +244,24 @@ - auth.errors.unknown - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    @@ -248,7 +287,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -258,6 +297,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    @@ -341,7 +385,7 @@ ### 🟡 common.json -**46/61** +**46/75**
    Missing @@ -357,10 +401,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    @@ -396,7 +454,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -406,6 +464,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    @@ -459,7 +522,7 @@ ### 🔴 common.json -**0/61** +**0/75** ### 🔴 contributions.json @@ -475,7 +538,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -485,6 +548,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    @@ -623,7 +691,7 @@ ### 🟡 common.json -**46/61** +**46/75**
    Missing @@ -639,10 +707,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    @@ -677,7 +759,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -687,6 +769,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    @@ -748,7 +835,7 @@ ### 🟡 common.json -**35/61** +**35/75**
    Missing @@ -764,10 +851,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload - weapon.category.SHOOTERS - weapon.category.BLASTERS - weapon.category.ROLLERS @@ -813,7 +914,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -823,6 +924,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    @@ -903,7 +1009,7 @@ ### 🟡 common.json -**48/61** +**48/75**
    Missing @@ -917,10 +1023,24 @@ - auth.errors.unknown - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    @@ -946,7 +1066,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -956,6 +1076,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    @@ -979,9 +1104,21 @@ ## /ru (🟡 In progress) -### 🔴 analyzer.json +### 🟡 analyzer.json -**0/107** +**101/107** + +
    +Missing + +- objCalcAd +- stat.specialLostSplattedByRP +- stat.quickRespawnTimeSplattedByRP +- damage.NORMAL_MAX_FULL_CHARGE +- dmgHtdExplanation +- noDmgData + +
    ### 🟢 badges.json @@ -991,64 +1128,38 @@ **11/11** -### 🟡 calendar.json +### 🟢 calendar.json -**44/46** - -
    -Missing - -- createMapList -- forms.mapPool - -
    +**46/46** ### 🟡 common.json -**35/61** +**60/75**
    Missing -- pages.s2 -- pages.analyzer -- pages.maps - pages.object-damage-calculator -- auth.errors.aborted -- auth.errors.failed -- auth.errors.discordPermissions -- auth.errors.unknown -- actions.loadMore -- actions.copyToClipboard -- actions.close -- maps.createMapList -- maps.halfSz -- maps.mapPool -- maps.tournamentMaplist -- weapon.category.SHOOTERS -- weapon.category.BLASTERS -- weapon.category.ROLLERS -- weapon.category.BRUSHES -- weapon.category.CHARGERS -- weapon.category.SLOSHERS -- weapon.category.SPLATLINGS -- weapon.category.DUALIES -- weapon.category.BRELLAS -- weapon.category.STRINGERS -- weapon.category.SPLATANAS +- actions.loading +- actions.clear +- actions.selectAll +- actions.search +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload
    -### 🟡 contributions.json +### 🟢 contributions.json -**5/6** - -
    -Missing - -- translation - -
    +**6/6** ### 🟢 faq.json @@ -1056,60 +1167,33 @@ ### 🟡 front.json -**8/12** +**11/12**
    Missing -- buildsGoTo -- analyzer.description -- maps.description - object-damage-calculator.description
    ### 🟡 game-misc.json -**12/17** +**17/22**
    Missing -- MODE_SHORT_TW -- MODE_SHORT_SZ -- MODE_SHORT_TC -- MODE_SHORT_RM -- MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB
    -### 🟡 user.json +### 🟢 user.json -**7/25** - -
    -Missing - -- customUrl -- ign -- ign.short -- stickSens -- motionSens -- motion -- stick -- sens -- results.title -- results.participants -- results.highlights -- results.nonHighlights -- results.highlights.choose -- results.highlights.explanation -- forms.errors.invalidCustomUrl.numbers -- forms.errors.invalidCustomUrl.strangeCharacter -- forms.errors.invalidCustomUrl.duplicate -- forms.errors.invalidSens - -
    +**25/25** --- @@ -1160,7 +1244,7 @@ ### 🟡 common.json -**35/61** +**35/75**
    Missing @@ -1176,10 +1260,24 @@ - actions.loadMore - actions.copyToClipboard - actions.close +- actions.loading +- actions.clear +- actions.selectAll +- actions.search - maps.createMapList - maps.halfSz - maps.mapPool - maps.tournamentMaplist +- maps.template +- maps.template.none +- maps.template.event +- maps.template.presets +- maps.template.yourRecentEvents +- maps.template.preset.ANARCHY +- maps.template.preset.ALL +- maps.template.preset.onlyMode +- forms.errors.noSearchMatches +- errors.genericReload - weapon.category.SHOOTERS - weapon.category.BLASTERS - weapon.category.ROLLERS @@ -1225,7 +1323,7 @@ ### 🟡 game-misc.json -**12/17** +**12/22**
    Missing @@ -1235,6 +1333,11 @@ - MODE_SHORT_TC - MODE_SHORT_RM - MODE_SHORT_CB +- MODE_LONG_TW +- MODE_LONG_SZ +- MODE_LONG_TC +- MODE_LONG_RM +- MODE_LONG_CB