Map pool on tournament page

This commit is contained in:
Kalle
2022-10-13 21:39:40 +03:00
parent 0f2a5ee728
commit 8470bf228e
9 changed files with 198 additions and 93 deletions

View File

@@ -6,6 +6,7 @@
{ "name": "builds", "url": "builds" },
{ "name": "analyzer", "url": "analyzer" },
{ "name": "calendar", "url": "calendar" },
{ "name": "maps", "url": "maps" },
{ "name": "badges", "url": "badges" },
{
"name": "plus",

View File

@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
import { z } from "zod";
import { Avatar } from "~/components/Avatar";
import { LinkButton } from "~/components/Button";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { Section } from "~/components/Section";
import { db } from "~/db";
@@ -23,21 +24,28 @@ import {
canEditCalendarEvent,
canReportCalendarEventWinners,
} from "~/permissions";
import styles from "~/styles/calendar-event.css";
import calendarStyles from "~/styles/calendar-event.css";
import mapsStyles from "~/styles/maps.css";
import { databaseTimestampToDate } from "~/utils/dates";
import { notFoundIfFalsy } from "~/utils/remix";
import { discordFullName, makeTitle, placementString } from "~/utils/strings";
import {
calendarEditPage,
calendarReportWinnersPage,
mapsPage,
navIconUrl,
resolveBaseUrl,
userPage,
} from "~/utils/urls";
import { actualNumber, id } from "~/utils/zod";
import { MapPoolSelector } from "../components/MapPoolSelector";
import { Tags } from "../components/Tags";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
return [
{ rel: "stylesheet", href: calendarStyles },
{ rel: "stylesheet", href: mapsStyles },
];
};
export const meta: MetaFunction = (args) => {
@@ -52,7 +60,7 @@ export const meta: MetaFunction = (args) => {
};
export const handle = {
i18n: "calendar",
i18n: ["calendar", "game-misc"],
};
export const loader = async ({ params, request }: LoaderArgs) => {
@@ -67,6 +75,7 @@ export const loader = async ({ params, request }: LoaderArgs) => {
badgePrizes: db.calendarEvents.findBadgesByEventId(parsedParams.id),
title: makeTitle([event.name, t("pages.calendar")]),
results: db.calendarEvents.findResultsByEventId(parsedParams.id),
mapPool: db.calendarEvents.findMapPoolByEventId(parsedParams.id),
});
};
@@ -159,6 +168,7 @@ export default function CalendarEventPage() {
</div>
</section>
<Results />
<MapPoolInfo />
<Description />
</Main>
);
@@ -222,6 +232,30 @@ function Results() {
);
}
function MapPoolInfo() {
const { t } = useTranslation(["calendar"]);
const data = useLoaderData<typeof loader>();
if (!data.mapPool) return null;
return (
<Section title="Map pool">
<div className="event__map-pool-section">
<MapPoolSelector mapPool={data.mapPool} />
<LinkButton
className="event__create-map-list-link"
to={mapsPage(data.event.eventId)}
variant="outlined"
tiny
>
<Image alt="" path={navIconUrl("maps")} width={22} height={22} />
{t("calendar:createMapList")}
</LinkButton>
</div>
</Section>
);
}
function Description() {
const { t } = useTranslation();
const data = useLoaderData<typeof loader>();

View File

@@ -0,0 +1,97 @@
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Image } from "~/components/Image";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import { modes, stageIds } from "~/modules/in-game-lists";
import { type MapPool } from "~/modules/map-pool-serializer";
import { modeImageUrl, stageImageUrl } from "~/utils/urls";
// xxx: maybe better mobile layout?
export function MapPoolSelector({
mapPool,
handleMapPoolChange,
}: {
mapPool: MapPool;
handleMapPoolChange?: ({
mode,
stageId,
}: {
mode: ModeShort;
stageId: StageId;
}) => void;
}) {
const { t } = useTranslation(["game-misc", "calendar"]);
const isPresentational = !handleMapPoolChange;
const stageRowIsVisible = (stageId: StageId) => {
if (!isPresentational) return true;
return modes.some((mode) => mapPool[mode.short].includes(stageId));
};
return (
<div className="stack md">
{stageIds.filter(stageRowIsVisible).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] as StageId[]).includes(
stageId
);
if (isPresentational && !selected) return null;
if (isPresentational && selected) {
return (
<Image
key={mode.short}
className={clsx("maps__mode", {
selected,
})}
alt={mode.long}
path={modeImageUrl(mode.short)}
width={33}
height={33}
/>
);
}
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>
);
}

View File

@@ -1,32 +1,52 @@
import { Form, useLoaderData } from "@remix-run/react";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import * as React from "react";
import type { Badge as BadgeType, CalendarEventTag } from "~/db/types";
import { CALENDAR_EVENT } from "~/constants";
import { Button } from "~/components/Button";
import type { SerializeFrom } from "@remix-run/node";
import {
json,
redirect,
type MetaFunction,
type ActionFunction,
type LinksFunction,
type LoaderArgs,
type MetaFunction,
} from "@remix-run/node";
import calendarNewStyles from "~/styles/calendar-new.css";
import mapsStyles from "~/styles/maps.css";
import { Form, useLoaderData } from "@remix-run/react";
import clsx from "clsx";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { Badge } from "~/components/Badge";
import { Button } from "~/components/Button";
import { DateInput } from "~/components/DateInput";
import { FormMessage } from "~/components/FormMessage";
import { TrashIcon } from "~/components/icons/Trash";
import { Input } from "~/components/Input";
import { FormMessage } from "~/components/FormMessage";
import { useIsMounted } from "~/hooks/useIsMounted";
import { Tags } from "./components/Tags";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { Toggle } from "~/components/Toggle";
import { CALENDAR_EVENT } from "~/constants";
import { db } from "~/db";
import type { Badge as BadgeType, CalendarEventTag } from "~/db/types";
import { useIsMounted } from "~/hooks/useIsMounted";
import { requireUser } from "~/modules/auth";
import { Badge } from "~/components/Badge";
import { z } from "zod";
import { i18next } from "~/modules/i18n";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import {
mapPoolToSerializedString,
serializedStringToMapPool,
type MapPool,
} from "~/modules/map-pool-serializer";
import { canEditCalendarEvent } from "~/permissions";
import calendarNewStyles from "~/styles/calendar-new.css";
import mapsStyles from "~/styles/maps.css";
import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
} from "~/utils/dates";
import {
badRequestIfFalsy,
parseRequestFormData,
validate,
} from "~/utils/remix";
import { makeTitle } from "~/utils/strings";
import { calendarEventPage } from "~/utils/urls";
import {
actualNumber,
date,
@@ -37,29 +57,8 @@ import {
safeJSONParse,
toArray,
} from "~/utils/zod";
import {
badRequestIfFalsy,
parseRequestFormData,
validate,
} from "~/utils/remix";
import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
} from "~/utils/dates";
import { calendarEventPage, modeImageUrl, stageImageUrl } from "~/utils/urls";
import { makeTitle } from "~/utils/strings";
import { i18next } from "~/modules/i18n";
import { canEditCalendarEvent } from "~/permissions";
import { DateInput } from "~/components/DateInput";
import type { ModeShort, StageId } from "~/modules/in-game-lists";
import { modes, stageIds } from "~/modules/in-game-lists";
import { Image } from "~/components/Image";
import {
type MapPool,
serializedStringToMapPool,
} from "~/modules/map-pool-serializer";
import { mapPoolToSerializedString } from "~/modules/map-pool-serializer";
import { Toggle } from "~/components/Toggle";
import { MapPoolSelector } from "./components/MapPoolSelector";
import { Tags } from "./components/Tags";
const MIN_DATE = new Date(Date.UTC(2015, 4, 28));
@@ -235,7 +234,7 @@ export default function CalendarNewEventPage() {
<DiscordLinkInput />
<TagsAdder />
<BadgesAdder />
<MapPoolSelector />
<MapPoolSection />
<Button type="submit" className="mt-4" data-cy="submit-button">
{t("actions.submit")}
</Button>
@@ -534,7 +533,7 @@ const DEFAULT_MAP_POOL = {
RM: [],
TW: [],
};
function MapPoolSelector() {
function MapPoolSection() {
const { t } = useTranslation(["game-misc", "calendar"]);
const data = useLoaderData<typeof loader>();
@@ -578,56 +577,10 @@ function MapPoolSelector() {
<div className="stack md">
<Toggle checked={includeMapPool} setChecked={setIncludeMapPool} tiny />
{includeMapPool && (
<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] as StageId[]
).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>
<MapPoolSelector
mapPool={mapPool}
handleMapPoolChange={handleMapPoolChange}
/>
)}
</div>
</div>

View File

@@ -17,6 +17,24 @@
height: 1.25rem;
}
.event__map-pool-section {
display: flex;
max-width: 32rem;
flex-direction: column;
padding: var(--s-2);
margin: 0 auto;
gap: var(--s-4);
}
.event__create-map-list-link {
display: flex;
width: max-content;
align-items: center;
margin: 0 auto;
font-size: var(--fonts-xs);
gap: var(--s-1-5);
}
.event__author {
display: flex;
align-items: center;

BIN
public/img/layout/maps.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
public/img/layout/maps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -8,6 +8,7 @@
"participatedCount": "{{count}} teams participated",
"members": "Members",
"results": "Results",
"createMapList": "Create map list",
"forms.dates": "Dates",
"forms.bracketUrl": "Bracket URL",

View File

@@ -8,6 +8,7 @@
"pages.faq": "FAQ",
"pages.builds": "Builds",
"pages.analyzer": "Build Analyzer",
"pages.maps": "Map Lists",
"header.profile": "Profile",
"header.logout": "Log out",