mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
Improve MapPoolSelector UI
This commit is contained in:
@@ -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 (
|
||||
<fieldset className={className}>
|
||||
<legend>{t("maps.mapPool")}</legend>
|
||||
<div className="stack md">
|
||||
<div className="stack horizontal sm justify-end">
|
||||
{handleRemoval && (
|
||||
<Button variant="minimal" onClick={handleRemoval}>
|
||||
{t("actions.remove")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="minimal-destructive"
|
||||
disabled={mapPool.isEmpty()}
|
||||
onClick={handleClear}
|
||||
>
|
||||
{t("actions.clear")}
|
||||
</Button>
|
||||
</div>
|
||||
<MapPoolStages
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleStageModesChange}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="stack md">
|
||||
{stageIds.filter(stageRowIsVisible).map((stageId) => (
|
||||
@@ -40,8 +136,14 @@ export function MapPoolSelector({
|
||||
width={80}
|
||||
height={45}
|
||||
/>
|
||||
<div className="maps__stage-name-row">
|
||||
<div>{t(`game-misc:STAGE_${stageId}`)}</div>
|
||||
<div
|
||||
className="maps__stage-name-row"
|
||||
role="group"
|
||||
aria-labelledby={`${id}-stage-name-${stageId}`}
|
||||
>
|
||||
<div id={`${id}-stage-name-${stageId}`}>
|
||||
{t(`game-misc:STAGE_${stageId}`)}
|
||||
</div>
|
||||
<div className="maps__mode-buttons-container">
|
||||
{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}
|
||||
>
|
||||
<Image
|
||||
className={clsx("maps__mode", {
|
||||
selected,
|
||||
})}
|
||||
alt={mode.long}
|
||||
alt={t(`game-misc:MODE_LONG_${mode.short}`)}
|
||||
path={modeImageUrl(mode.short)}
|
||||
width={20}
|
||||
height={20}
|
||||
@@ -85,6 +191,28 @@ export function MapPoolSelector({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!isPresentational &&
|
||||
(mapPool.hasStage(stageId) ? (
|
||||
<Button
|
||||
key="clear"
|
||||
onClick={() => handleStageClear(stageId)}
|
||||
icon={<CrossIcon />}
|
||||
variant="minimal"
|
||||
aria-label={t("common:actions.remove")}
|
||||
title={t("common:actions.remove")}
|
||||
tiny
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
key="select-all"
|
||||
onClick={() => handleStageAdd(stageId)}
|
||||
icon={<ArrowLongLeftIcon />}
|
||||
variant="minimal"
|
||||
aria-label={t("common:actions.selectAll")}
|
||||
title={t("common:actions.selectAll")}
|
||||
tiny
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
18
app/components/icons/ArrowLongLeft.tsx
Normal file
18
app/components/icons/ArrowLongLeft.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function ArrowLongLeftIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6.75 15.75L3 12m0 0l3.75-3.75M3 12h18"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
10
app/hooks/useOnce.ts
Normal file
10
app/hooks/useOnce.ts
Normal file
@@ -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<T>(factory: () => T) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useMemo(factory, []);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Pick<MapPoolMap, "stageId" | "mode">>;
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Section title="Map pool">
|
||||
<div className="event__map-pool-section">
|
||||
<MapPoolSelector mapPool={new MapPool(data.mapPool)} />
|
||||
<MapPoolStages mapPool={new MapPool(data.mapPool)} />
|
||||
<LinkButton
|
||||
className="event__create-map-list-link"
|
||||
to={mapsPage(data.event.eventId)}
|
||||
to={readonlyMapsPage(data.event.eventId)}
|
||||
variant="outlined"
|
||||
tiny
|
||||
>
|
||||
|
||||
@@ -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<typeof loader>();
|
||||
const [mapPool, setMapPool] = React.useState<MapPool>(
|
||||
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 ? (
|
||||
<>
|
||||
<input type="hidden" name="pool" value={mapPool.serialized} />
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{includeMapPool && (
|
||||
<input type="hidden" name="pool" value={mapPool.serialized} />
|
||||
)}
|
||||
<Label>{t("calendar:forms.mapPool")}</Label>
|
||||
<div className="stack md">
|
||||
<Toggle checked={includeMapPool} setChecked={setIncludeMapPool} tiny />
|
||||
{includeMapPool && (
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MapPoolSelector
|
||||
className="w-full"
|
||||
mapPool={mapPool}
|
||||
handleRemoval={() => setIncludeMapPool(false)}
|
||||
handleMapPoolChange={setMapPool}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<label htmlFor={id}>{t("common:maps.mapPool")}</label>
|
||||
<Button
|
||||
id={id}
|
||||
variant="outlined"
|
||||
tiny
|
||||
onClick={() => setIncludeMapPool(true)}
|
||||
>
|
||||
{t("common:actions.add")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<typeof loader>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { mapPool, handleMapPoolChange } = useSearchParamMapPool();
|
||||
const { mapPool, handleMapPoolChange, readonly, switchToEditMode } =
|
||||
useSearchParamPersistedMapPool();
|
||||
|
||||
return (
|
||||
<Main className="maps__container stack lg">
|
||||
{data.calendarEvent && !searchParams.has("pool") && (
|
||||
<div className="maps__pool-info">
|
||||
{t("common:maps.mapPool")}:{" "}
|
||||
<Link to={calendarEventPage(data.calendarEvent.id)}>
|
||||
{data.calendarEvent.name}
|
||||
</Link>
|
||||
{searchParams.has("readonly") && data.calendarEvent && (
|
||||
<div className="maps__pool-meta">
|
||||
<div className="maps__pool-info">
|
||||
{t("common:maps.mapPool")}:{" "}
|
||||
{
|
||||
<Link to={calendarEventPage(data.calendarEvent.id)}>
|
||||
{data.calendarEvent.name}
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={switchToEditMode}
|
||||
tiny
|
||||
icon={<EditIcon />}
|
||||
>
|
||||
{t("common:actions.edit")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
/>
|
||||
{readonly ? (
|
||||
<MapPoolStages mapPool={mapPool} />
|
||||
) : (
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
href={ipLabsMaps(mapPool.serialized)}
|
||||
target="_blank"
|
||||
@@ -122,41 +133,26 @@ export default function MapListPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function useSearchParamMapPool() {
|
||||
function useSearchParamPersistedMapPool() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
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 (
|
||||
<div className="maps__map-list-creator">
|
||||
<div className="maps__toggle-container">
|
||||
<Label>{t("common:maps.halfSz")}</Label>
|
||||
<Toggle checked={szEveryOther} setChecked={setSzEveryOther} tiny />
|
||||
</div>
|
||||
<Button onClick={handleCreateMaplist}>
|
||||
<Button onClick={handleCreateMaplist} disabled={disabled}>
|
||||
{t("common:maps.createMapList")}
|
||||
</Button>
|
||||
{mapList && (
|
||||
@@ -206,7 +216,12 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
|
||||
<ol className="maps__map-list">
|
||||
{mapList.map(({ mode, stageId }, i) => (
|
||||
<li key={i}>
|
||||
{t(`game-misc:MODE_SHORT_${mode}`)}{" "}
|
||||
<abbr
|
||||
className="maps__mode-abbr"
|
||||
title={t(`game-misc:MODE_LONG_${mode}`)}
|
||||
>
|
||||
{t(`game-misc:MODE_SHORT_${mode}`)}
|
||||
</abbr>{" "}
|
||||
{t(`game-misc:STAGE_${stageId}`)}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.calendar-new__container {
|
||||
max-width: 32rem;
|
||||
max-width: 38rem;
|
||||
}
|
||||
|
||||
.calendar-new__select {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,10 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user