mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-06-01 00:13:20 -05:00
302 lines
7.8 KiB
TypeScript
302 lines
7.8 KiB
TypeScript
import type {
|
|
LinksFunction,
|
|
LoaderArgs,
|
|
MetaFunction,
|
|
SerializeFrom,
|
|
} from "@remix-run/node";
|
|
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,
|
|
type StageId,
|
|
} from "~/modules/in-game-lists";
|
|
import {
|
|
generateMapList,
|
|
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 styles from "~/styles/maps.css";
|
|
import { makeTitle } from "~/utils/strings";
|
|
import {
|
|
calendarEventPage,
|
|
ipLabsMaps,
|
|
modeImageUrl,
|
|
stageImageUrl,
|
|
} from "~/utils/urls";
|
|
|
|
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
|
|
|
|
export const unstable_shouldReload: ShouldReloadFunction = () => false;
|
|
|
|
export const links: LinksFunction = () => {
|
|
return [{ rel: "stylesheet", href: styles }];
|
|
};
|
|
|
|
export const meta: MetaFunction = (args) => {
|
|
const data = args.data as SerializeFrom<typeof loader> | null;
|
|
|
|
if (!data) return {};
|
|
|
|
return {
|
|
title: data.title,
|
|
};
|
|
};
|
|
|
|
export const handle = {
|
|
i18n: "game-misc",
|
|
};
|
|
|
|
export const loader = async ({ request }: LoaderArgs) => {
|
|
const url = new URL(request.url);
|
|
const calendarEventId = url.searchParams.get("eventId");
|
|
const t = await i18next.getFixedT(request);
|
|
|
|
const event = calendarEventId
|
|
? db.calendarEvents.findById(Number(calendarEventId))
|
|
: undefined;
|
|
|
|
return {
|
|
calendarEvent: event
|
|
? {
|
|
id: event.eventId,
|
|
name: event.name,
|
|
}
|
|
: undefined,
|
|
mapPool: event
|
|
? db.calendarEvents.findMapPoolByEventId(event.eventId)
|
|
: null,
|
|
title: makeTitle([t("pages.maps")]),
|
|
};
|
|
};
|
|
|
|
const DEFAULT_MAP_POOL = {
|
|
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();
|
|
|
|
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>
|
|
</div>
|
|
)}
|
|
<MapPoolSelector
|
|
mapPool={mapPool}
|
|
handleMapPoolChange={handleMapPoolChange}
|
|
/>
|
|
<a
|
|
href={ipLabsMaps(mapPoolToSerializedString(mapPool))}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="maps__tournament-map-list-link"
|
|
>
|
|
{t("common:maps.tournamentMaplist")}
|
|
</a>
|
|
<MapListCreator mapPool={mapPool} />
|
|
</Main>
|
|
);
|
|
}
|
|
|
|
function useSearchParamMapPool() {
|
|
const data = useLoaderData<typeof loader>();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const mapPool = (() => {
|
|
if (searchParams.has("pool")) {
|
|
return serializedStringToMapPool(searchParams.get("pool")!);
|
|
}
|
|
|
|
if (data?.mapPool) {
|
|
return data.mapPool;
|
|
}
|
|
|
|
return DEFAULT_MAP_POOL;
|
|
})();
|
|
|
|
const handleMapPoolChange = ({
|
|
mode,
|
|
stageId,
|
|
}: {
|
|
mode: ModeShort;
|
|
stageId: StageId;
|
|
}) => {
|
|
const newMapPool = mapPool[mode].includes(stageId)
|
|
? {
|
|
...mapPool,
|
|
[mode]: mapPool[mode].filter((id) => id !== stageId),
|
|
}
|
|
: {
|
|
...mapPool,
|
|
[mode]: [...mapPool[mode], stageId],
|
|
};
|
|
|
|
setSearchParams(
|
|
{
|
|
pool: mapPoolToSerializedString(newMapPool),
|
|
},
|
|
{ replace: true, state: { scroll: false } }
|
|
);
|
|
};
|
|
|
|
return {
|
|
mapPool,
|
|
handleMapPoolChange,
|
|
};
|
|
}
|
|
|
|
function MapPoolSelector({
|
|
mapPool,
|
|
handleMapPoolChange,
|
|
}: {
|
|
mapPool: MapPool;
|
|
handleMapPoolChange: (args: { mode: ModeShort; stageId: StageId }) => void;
|
|
}) {
|
|
const { t } = useTranslation(["game-misc"]);
|
|
|
|
return (
|
|
<div className="stack md">
|
|
{stageIds.map((stageId) => (
|
|
<div key={stageId} className="maps__stage-row">
|
|
<Image
|
|
className="maps__stage-image"
|
|
alt=""
|
|
path={stageImageUrl(stageId)}
|
|
width={80}
|
|
height={45}
|
|
/>
|
|
<div className="maps__stage-name-row">
|
|
<div>{t(`game-misc:STAGE_${stageId}`)}</div>
|
|
<div className="maps__mode-buttons-container">
|
|
{modes.map((mode) => {
|
|
const selected = mapPool[mode.short].includes(stageId);
|
|
|
|
return (
|
|
<button
|
|
key={mode.short}
|
|
className={clsx("maps__mode-button", "outline-theme", {
|
|
selected,
|
|
})}
|
|
onClick={() =>
|
|
handleMapPoolChange({ mode: mode.short, stageId })
|
|
}
|
|
type="button"
|
|
>
|
|
<Image
|
|
className={clsx("maps__mode", {
|
|
selected,
|
|
})}
|
|
alt={mode.long}
|
|
path={modeImageUrl(mode.short)}
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// xxx: crashes if only one map in mode
|
|
function MapListCreator({ mapPool }: { mapPool: MapPool }) {
|
|
const { t } = useTranslation(["game-misc", "common"]);
|
|
const [mapList, setMapList] = React.useState<ModeWithStage[]>();
|
|
const [szEveryOther, setSzEveryOther] = React.useState(false);
|
|
const [, copyToClipboard] = useCopyToClipboard();
|
|
|
|
const handleCreateMaplist = () => {
|
|
const [list] = generateMapList(
|
|
mapPool,
|
|
modesOrder(
|
|
szEveryOther ? "SZ_EVERY_OTHER" : "EQUAL",
|
|
mapPoolToNonEmptyModes(mapPool)
|
|
),
|
|
[AMOUNT_OF_MAPS_IN_MAP_LIST]
|
|
);
|
|
|
|
invariant(list);
|
|
|
|
setMapList(list);
|
|
};
|
|
|
|
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}>
|
|
{t("common:maps.createMapList")}
|
|
</Button>
|
|
{mapList && (
|
|
<>
|
|
<ol className="maps__map-list">
|
|
{mapList.map(({ mode, stageId }, i) => (
|
|
<li key={i}>
|
|
{t(`game-misc:MODE_SHORT_${mode}`)}{" "}
|
|
{t(`game-misc:STAGE_${stageId}`)}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
<Button
|
|
tiny
|
|
variant="outlined"
|
|
onClick={() =>
|
|
copyToClipboard(
|
|
mapList
|
|
.map(
|
|
({ mode, stageId }, i) =>
|
|
`${i + 1}) ${t(`game-misc:MODE_SHORT_${mode}`)} ${t(
|
|
`game-misc:STAGE_${stageId}`
|
|
)}`
|
|
)
|
|
.join("\n")
|
|
)
|
|
}
|
|
>
|
|
{t("common:actions.copyToClipboard")}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|