mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-29 22:58:18 -05:00
Map pool from server
This commit is contained in:
18
app/components/icons/Upload.tsx
Normal file
18
app/components/icons/Upload.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
app/db/models/maps/findMapPoolByCode.sql
Normal file
22
app/db/models/maps/findMapPoolByCode.sql
Normal 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"
|
||||
@@ -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">>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user