mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
Map pool on tournament page
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>();
|
||||
|
||||
97
app/routes/calendar/components/MapPoolSelector.tsx
Normal file
97
app/routes/calendar/components/MapPoolSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
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
BIN
public/img/layout/maps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -8,6 +8,7 @@
|
||||
"participatedCount": "{{count}} teams participated",
|
||||
"members": "Members",
|
||||
"results": "Results",
|
||||
"createMapList": "Create map list",
|
||||
|
||||
"forms.dates": "Dates",
|
||||
"forms.bracketUrl": "Bracket URL",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user