diff --git a/app/components/icons/Upload.tsx b/app/components/icons/Upload.tsx new file mode 100644 index 000000000..51f463e63 --- /dev/null +++ b/app/components/icons/Upload.tsx @@ -0,0 +1,18 @@ +export function UploadIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/db/models/maps/findMapPoolByCode.sql b/app/db/models/maps/findMapPoolByCode.sql new file mode 100644 index 000000000..694915493 --- /dev/null +++ b/app/db/models/maps/findMapPoolByCode.sql @@ -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" diff --git a/app/db/models/maps/queries.server.ts b/app/db/models/maps/queries.server.ts index 13a11b794..e1201ce75 100644 --- a/app/db/models/maps/queries.server.ts +++ b/app/db/models/maps/queries.server.ts @@ -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>; + }; +} diff --git a/app/modules/map-pool-serializer/types.ts b/app/modules/map-pool-serializer/types.ts index c3e416dc0..f8a7c1189 100644 --- a/app/modules/map-pool-serializer/types.ts +++ b/app/modules/map-pool-serializer/types.ts @@ -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; diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 4a84e9394..bf7d51552 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -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
Coming soon :)
; + } + return (
@@ -97,10 +122,26 @@ export default function MapListPage() { } function useSearchParamMapPool() { + const data = useLoaderData(); 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(); const { t } = useTranslation(["game-misc"]); - if ( - process.env.NODE_ENV !== "development" && - user?.discordId !== ADMIN_DISCORD_ID - ) { - return
Coming soon :)
; - } + const editMode = !data; return (
@@ -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" > (); + + if (data) { + return ( +
+ Code: {data.code} - Made by: {discordFullName(data.owner)} +
+ ); + } + 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} />
- diff --git a/app/styles/maps.css b/app/styles/maps.css index 008206148..065dd7051 100644 --- a/app/styles/maps.css +++ b/app/styles/maps.css @@ -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; diff --git a/app/utils/urls.ts b/app/utils/urls.ts index 5df411573..e029f31e8 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -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;