Map pool from server

This commit is contained in:
Kalle
2022-10-09 19:06:22 +03:00
parent e00c7cf531
commit 6770b8e214
7 changed files with 145 additions and 17 deletions

View File

@@ -0,0 +1,18 @@
export function UploadIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"
/>
</svg>
);
}

View File

@@ -0,0 +1,22 @@
select
"MapPool"."id",
"MapPool"."code",
"MapPool"."ownerId",
"User"."discordName",
"User"."discordDiscriminator",
json_group_array(
json_object(
'stageId',
"MapPoolMap"."stageId",
'mode',
"MapPoolMap"."mode"
)
) as "maps"
from
"MapPool"
left join "MapPoolMap" on "MapPoolMap"."mapPoolId" = "MapPool"."id"
left join "User" on "User"."id" = "MapPool"."ownerId"
where
code = lower(@code)
group by
"MapPool"."id"

View File

@@ -1,10 +1,12 @@
import { sql } from "~/db/sql";
import type { MapPool, MapPoolMap } from "~/db/types";
import type { MapPool, MapPoolMap, User } from "~/db/types";
import createMapPoolSql from "./createMapPool.sql";
import createMapPoolMapSql from "./createMapPoolMap.sql";
import findMapPoolByCodeSql from "./findMapPoolByCode.sql";
const createMapPoolStm = sql.prepare(createMapPoolSql);
const createMapPoolMapStm = sql.prepare(createMapPoolMapSql);
const findMapPoolByCodeStm = sql.prepare(findMapPoolByCodeSql);
export const addMapPool = sql.transaction(
({
@@ -26,3 +28,28 @@ export const addMapPool = sql.transaction(
}
}
);
export function findMapPoolByCode(code: MapPool["code"]) {
const row = findMapPoolByCodeStm.get({ code });
if (!row) return;
return {
id: row.id,
code: row.code,
owner: {
id: row.ownerId,
discordName: row.discordName,
discordDiscriminator: row.discordDiscriminator,
},
maps: JSON.parse(row.maps),
} as {
id: MapPool["id"];
code: MapPool["code"];
owner: {
id: User["id"];
discordName: User["discordName"];
discordDiscriminator: User["discordDiscriminator"];
};
maps: Array<Pick<MapPoolMap, "mode" | "stageId">>;
};
}

View File

@@ -1,4 +1,5 @@
import type { ModeShort } from "../in-game-lists";
import type { StageId } from "../in-game-lists";
// xxx: rename - same name as DB table
export type MapPool = Record<ModeShort, StageId[]>;

View File

@@ -1,4 +1,9 @@
import type { ActionFunction, LinksFunction } from "@remix-run/node";
import type {
ActionFunction,
LinksFunction,
LoaderArgs,
} from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { useTranslation } from "react-i18next";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
@@ -11,9 +16,9 @@ import {
import { modes, stageIds } from "~/modules/in-game-lists";
import type { MapPool } from "~/modules/map-pool-serializer/types";
import styles from "~/styles/maps.css";
import { modeImageUrl, stageImageUrl } from "~/utils/urls";
import { mapsPage, modeImageUrl, stageImageUrl } from "~/utils/urls";
import clsx from "clsx";
import { Form, useSearchParams } from "@remix-run/react";
import { Form, useLoaderData, useSearchParams } from "@remix-run/react";
import {
mapPoolToSerializedString,
serializedStringToMapPool,
@@ -35,6 +40,8 @@ import invariant from "tiny-invariant";
import { z } from "zod";
import { parseRequestFormData } from "~/utils/remix";
import { db } from "~/db";
import { discordFullName } from "~/utils/strings";
import { UploadIcon } from "~/components/icons/Upload";
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
@@ -47,6 +54,7 @@ export const handle = {
};
// xxx: next -> define user flow after submitting a map pool
// xxx: code can't have spaces or special characters also convert upper case to lower case
const mapsActionSchema = z.object({
code: z.string().min(MAPS.CODE_MIN_LENGTH).max(MAPS.CODE_MAX_LENGTH),
pool: z.string(),
@@ -70,7 +78,16 @@ export const action: ActionFunction = async ({ request }) => {
maps,
});
return null;
return redirect(mapsPage(data.code));
};
export const loader = ({ request }: LoaderArgs) => {
const url = new URL(request.url);
const code = url.searchParams.get("code");
if (!code) return null;
return db.maps.findMapPoolByCode(code) ?? null;
};
const DEFAULT_MAP_POOL = {
@@ -82,8 +99,16 @@ const DEFAULT_MAP_POOL = {
};
export default function MapListPage() {
const user = useUser();
const { mapPool, handleMapPoolChange } = useSearchParamMapPool();
if (
process.env.NODE_ENV !== "development" &&
user?.discordId !== ADMIN_DISCORD_ID
) {
return <Main>Coming soon :)</Main>;
}
return (
<Main className="maps__container stack lg">
<MapPoolLoaderSaver mapPool={mapPool} />
@@ -97,10 +122,26 @@ export default function MapListPage() {
}
function useSearchParamMapPool() {
const data = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const mapPool = searchParams.has("pool")
? serializedStringToMapPool(searchParams.get("pool")!)
: DEFAULT_MAP_POOL;
const mapPool = (() => {
if (data?.maps) {
return {
TW: data.maps.filter((m) => m.mode === "TW").map((m) => m.stageId),
SZ: data.maps.filter((m) => m.mode === "SZ").map((m) => m.stageId),
TC: data.maps.filter((m) => m.mode === "TC").map((m) => m.stageId),
CB: data.maps.filter((m) => m.mode === "CB").map((m) => m.stageId),
RM: data.maps.filter((m) => m.mode === "RM").map((m) => m.stageId),
};
}
if (searchParams.has("pool")) {
return serializedStringToMapPool(searchParams.get("pool")!);
}
return DEFAULT_MAP_POOL;
})();
const handleMapPoolChange = ({
mode,
@@ -140,15 +181,10 @@ function MapPoolSelector({
mapPool: MapPool;
handleMapPoolChange: (args: { mode: ModeShort; stageId: StageId }) => void;
}) {
const user = useUser();
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["game-misc"]);
if (
process.env.NODE_ENV !== "development" &&
user?.discordId !== ADMIN_DISCORD_ID
) {
return <Main>Coming soon :)</Main>;
}
const editMode = !data;
return (
<div className="stack md">
@@ -172,10 +208,12 @@ function MapPoolSelector({
key={mode.short}
className={clsx("maps__mode-button", "outline-theme", {
selected,
hidden: !editMode && !selected,
})}
onClick={() =>
handleMapPoolChange({ mode: mode.short, stageId })
}
disabled={!editMode}
type="button"
>
<Image
@@ -200,6 +238,16 @@ function MapPoolSelector({
// xxx: show "log in to save map pools" if not logged in
function MapPoolLoaderSaver({ mapPool }: { mapPool: MapPool }) {
const data = useLoaderData<typeof loader>();
if (data) {
return (
<div className="maps__pool_info">
Code: <code>{data.code}</code> - Made by: {discordFullName(data.owner)}
</div>
);
}
const hasChanges = (() => {
for (const mode of modesShort) {
if (mapPool[mode].length !== DEFAULT_MAP_POOL[mode].length) {
@@ -234,7 +282,11 @@ function MapPoolLoaderSaver({ mapPool }: { mapPool: MapPool }) {
maxLength={MAPS.CODE_MAX_LENGTH}
/>
</div>
<Button icon={<DownloadIcon />} variant="outlined" type="submit">
<Button
icon={hasChanges ? <UploadIcon /> : <DownloadIcon />}
variant="outlined"
type="submit"
>
{hasChanges ? "Save map pool" : "Load map pool"}
</Button>
</Form>

View File

@@ -44,6 +44,7 @@
background-color: var(--bg-darker);
border-radius: var(--rounded);
color: var(--theme);
opacity: 1 !important;
outline: initial;
}
@@ -71,6 +72,11 @@
gap: var(--s-4);
}
.maps__pool_info {
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
}
.maps__toggle-container {
--label-margin: 0;

View File

@@ -1,5 +1,5 @@
import slugify from "slugify";
import type { Badge, GearType, User } from "~/db/types";
import type { Badge, GearType, MapPool, User } from "~/db/types";
import type { ModeShort, weaponCategories } from "~/modules/in-game-lists";
import type {
Ability,
@@ -64,6 +64,8 @@ export const calendarEditPage = (eventId?: number) =>
`/calendar/new${eventId ? `?eventId=${eventId}` : ""}`;
export const calendarReportWinnersPage = (eventId: number) =>
`/calendar/${eventId}/report-winners`;
export const mapsPage = (code?: MapPool["code"]) =>
`/maps${code ? `?code=${code}` : ""}`;
export const articlePage = (slug: string) => `/a/${slug}`;
export const analyzerPage = (args?: {
weaponId: MainWeaponId;