import type { LinksFunction, LoaderArgs, V2_MetaFunction, SerializeFrom, } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; import { Link, useLoaderData, useSearchParams } from "@remix-run/react"; import * as React from "react"; import { useCopyToClipboard } from "react-use"; import invariant from "tiny-invariant"; import { Button } from "~/components/Button"; import { EditIcon } from "~/components/icons/Edit"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector"; import { Toggle } from "~/components/Toggle"; import { db } from "~/db"; import type { CalendarEvent } from "~/db/types"; import { useTranslation } from "~/hooks/useTranslation"; import { getUserId } from "~/modules/auth/user.server"; import { i18next } from "~/modules/i18n"; import { stageIds, type ModeWithStage } from "~/modules/in-game-lists"; import { generateMapList, mapPoolToNonEmptyModes, modesOrder, } from "~/modules/map-list-generator"; import { MapPool } from "~/modules/map-pool-serializer"; import styles from "~/styles/maps.css"; import { type SendouRouteHandle } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { calendarEventPage, ipLabsMaps, MAPS_URL, navIconUrl, } from "~/utils/urls"; const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2; export const shouldRevalidate: ShouldRevalidateFunction = ({ nextUrl }) => { const searchParams = new URL(nextUrl).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 }]; }; export const meta: V2_MetaFunction = (args) => { const data = args.data as SerializeFrom | null; if (!data) return []; return [{ title: data.title }]; }; export const handle: SendouRouteHandle = { i18n: "game-misc", breadcrumb: () => ({ imgPath: navIconUrl("maps"), href: MAPS_URL, type: "IMAGE", }), }; export const loader = async ({ request }: LoaderArgs) => { const user = await getUserId(request); 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, recentEventsWithMapPools: user ? db.calendarEvents.findRecentMapPoolsByAuthorId(user.id) : undefined, title: makeTitle([t("pages.maps")]), }; }; export default function MapListPage() { const { t } = useTranslation(["common"]); const data = useLoaderData(); const [searchParams] = useSearchParams(); const { mapPool, handleMapPoolChange, readonly, switchToEditMode } = useSearchParamPersistedMapPool(); return (
{searchParams.has("readonly") && data.calendarEvent && (
{t("common:maps.mapPool")}:{" "} { {data.calendarEvent.name} }
)} {readonly ? ( ) : ( )} {t("common:maps.tournamentMaplist")}
); } function useSearchParamPersistedMapPool() { const data = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); const [mapPool, setMapPool] = React.useState(() => { if (searchParams.has("pool")) { return new MapPool(searchParams.get("pool")!); } if (data.mapPool) { return new MapPool(data.mapPool); } return MapPool.ANARCHY; }); const handleMapPoolChange = ( newMapPool: MapPool, event?: Pick ) => { setMapPool(newMapPool); setSearchParams( event ? { eventId: event.id.toString() } : { pool: newMapPool.serialized, }, { replace: true, state: { scroll: false } } ); }; 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, }; } function MapListCreator({ mapPool }: { mapPool: MapPool }) { const { t } = useTranslation(["game-misc", "common"]); const [mapList, setMapList] = React.useState(); 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); }; const disabled = mapPool.isEmpty() || (szEveryOther && !mapPool.hasMode("SZ")); return (
{mapList && ( <>
    {mapList.map(({ mode, stageId }, i) => (
  1. {t(`game-misc:MODE_SHORT_${mode}`)} {" "} {t(`game-misc:STAGE_${stageId}`)}
  2. ))}
)}
); }