Merge branch 'rewrite' of https://github.com/Sendouc/sendou.ink into patch-2

This commit is contained in:
Kalle
2022-10-13 23:30:33 +03:00
34 changed files with 1386 additions and 374 deletions

View File

@@ -8,6 +8,7 @@ export function Input({
defaultValue,
leftAddon,
pattern,
list,
"data-cy": dataCy,
}: {
name: string;
@@ -17,6 +18,7 @@ export function Input({
defaultValue?: string;
leftAddon?: string;
pattern?: string;
list?: string;
"data-cy"?: string;
}) {
return (
@@ -27,8 +29,9 @@ export function Input({
minLength={minLength}
maxLength={maxLength}
defaultValue={defaultValue}
data-cy={dataCy}
pattern={pattern}
list={list}
data-cy={dataCy}
/>
</div>
);

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

@@ -4,7 +4,6 @@ import * as plusVotes from "./models/plusVotes/queries.server";
import * as badges from "./models/badges/queries.server";
import * as calendarEvents from "./models/calendar/queries.server";
import * as builds from "./models/builds/queries.server";
import * as maps from "./models/maps/queries.server";
export const db = {
users,
@@ -13,5 +12,4 @@ export const db = {
badges,
calendarEvents,
builds,
maps,
};

View File

@@ -0,0 +1,4 @@
insert into
"MapPoolMap" ("calendarEventId", "stageId", "mode")
values
(@calendarEventId, @stageId, @mode)

View File

@@ -0,0 +1,4 @@
delete from
"MapPoolMap"
where
"calendarEventId" = @calendarEventId

View File

@@ -0,0 +1,7 @@
select
"stageId",
"mode"
from
"MapPoolMap"
where
"calendarEventId" = @calendarEventId

View File

@@ -9,7 +9,9 @@ import type {
CalendarEventBadge,
CalendarEventResultTeam,
CalendarEventResultPlayer,
MapPoolMap,
} from "../../types";
import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator";
import createSql from "./create.sql";
import updateSql from "./update.sql";
@@ -31,6 +33,9 @@ import findBadgesByEventIdSql from "./findBadgesByEventId.sql";
import eventsToReportSql from "./eventsToReport.sql";
import recentWinnersSql from "./recentWinners.sql";
import upcomingEventsSql from "./upcomingEvents.sql";
import createMapPoolMapSql from "./createMapPoolMap.sql";
import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql";
import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql";
const createStm = sql.prepare(createSql);
const updateStm = sql.prepare(updateSql);
@@ -38,6 +43,9 @@ const createDateStm = sql.prepare(createDateSql);
const deleteDatesByEventIdStm = sql.prepare(deleteDatesByEventIdSql);
const createBadgeStm = sql.prepare(createBadgeSql);
const deleteBadgesByEventIdStm = sql.prepare(deleteBadgesByEventIdSql);
const createMapPoolMapStm = sql.prepare(createMapPoolMapSql);
const deleteMapPoolMapsStm = sql.prepare(deleteMapPoolMapsSql);
const findMapPoolByEventIdStm = sql.prepare(findMapPoolByEventIdSql);
export type CreateArgs = Pick<
CalendarEvent,
@@ -50,9 +58,15 @@ export type CreateArgs = Pick<
> & {
startTimes: Array<CalendarEventDate["startTime"]>;
badges: Array<CalendarEventBadge["badgeId"]>;
mapPoolMaps?: Array<Pick<MapPoolMap, "mode" | "stageId">>;
};
export const create = sql.transaction(
({ startTimes, badges, ...calendarEventArgs }: CreateArgs) => {
({
startTimes,
badges,
mapPoolMaps = [],
...calendarEventArgs
}: CreateArgs) => {
const createdEvent = createStm.get(calendarEventArgs) as CalendarEvent;
for (const startTime of startTimes) {
@@ -69,6 +83,13 @@ export const create = sql.transaction(
});
}
for (const mapPoolArgs of mapPoolMaps) {
createMapPoolMapStm.run({
calendarEventId: createdEvent.id,
...mapPoolArgs,
});
}
return createdEvent.id;
}
);
@@ -78,6 +99,7 @@ export const update = sql.transaction(
startTimes,
badges,
eventId,
mapPoolMaps = [],
...calendarEventArgs
}: Omit<CreateArgs, "authorId"> & { eventId: CalendarEvent["id"] }) => {
updateStm.run({ ...calendarEventArgs, eventId });
@@ -97,6 +119,14 @@ export const update = sql.transaction(
badgeId,
});
}
deleteMapPoolMapsStm.run({ calendarEventId: eventId });
for (const mapPoolArgs of mapPoolMaps) {
createMapPoolMapStm.run({
calendarEventId: eventId,
...mapPoolArgs,
});
}
}
);
@@ -410,6 +440,16 @@ export function findBadgesByEventId(eventId: CalendarEvent["id"]) {
>;
}
export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) {
const rows = findMapPoolByEventIdStm.all({ calendarEventId }) as Array<
Pick<MapPoolMap, "stageId" | "mode">
>;
if (rows.length === 0) return;
return mapPoolListToMapPoolObject(rows);
}
const eventsToReportStm = sql.prepare(eventsToReportSql);
export function eventsToReport(authorId?: CalendarEvent["authorId"]) {
if (!authorId) return [];

View File

@@ -1,4 +0,0 @@
insert into
"MapPool" ("code", "ownerId")
values
(@code, @ownerId) returning *

View File

@@ -1,4 +0,0 @@
insert into
"MapPoolMap" ("mapPoolId", "stageId", "mode")
values
(@mapPoolId, @stageId, @mode)

View File

@@ -1,22 +0,0 @@
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,55 +0,0 @@
import { sql } from "~/db/sql";
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(
({
ownerId,
code,
maps,
}: {
ownerId: MapPool["id"];
code: MapPool["code"];
maps: Array<Pick<MapPoolMap, "mode" | "stageId">>;
}) => {
const mapPool = createMapPoolStm.get({ ownerId, code }) as MapPool;
for (const args of maps) {
createMapPoolMapStm.run({
mapPoolId: mapPool.id,
...args,
});
}
}
);
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

@@ -155,14 +155,8 @@ export interface BuildAbility {
slotIndex: 0 | 1 | 2 | 3;
}
export interface MapPool {
id: number;
code: string;
ownerId: number;
}
export interface MapPoolMap {
mapPoolId: number;
calendarEventId?: number;
stageId: StageId;
mode: ModeShort;
}

View File

@@ -1,3 +1,3 @@
export { generateMapList } from "./map-list";
export { modesOrder } from "./modes";
export { mapPoolToNonEmptyModes } from "./utils";
export { mapPoolToNonEmptyModes, mapPoolListToMapPoolObject } from "./utils";

View File

@@ -1,3 +1,4 @@
import type { MapPoolMap } from "~/db/types";
import type { ModeShort } from "../in-game-lists";
import type { MapPool } from "../map-pool-serializer";
@@ -12,3 +13,21 @@ export function mapPoolToNonEmptyModes(mapPool: MapPool) {
return result;
}
export function mapPoolListToMapPoolObject(
mapPoolList: Array<Pick<MapPoolMap, "stageId" | "mode">>
) {
const result: MapPool = {
TW: [],
SZ: [],
TC: [],
RM: [],
CB: [],
};
for (const { stageId, mode } of mapPoolList) {
result[mode].push(stageId);
}
return result;
}

View File

@@ -1,5 +1,4 @@
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

@@ -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>();
@@ -233,9 +267,11 @@ function Description() {
<Avatar user={data.event} size="xs" />
{discordFullName(data.event)}
</div>
<div data-cy="event-description" className="whitespace-pre-wrap">
{data.event.description}
</div>
{data.event.description && (
<div data-cy="event-description" className="whitespace-pre-wrap">
{data.event.description}
</div>
)}
</div>
</Section>
);

View File

@@ -0,0 +1,96 @@
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";
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,31 +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 styles from "~/styles/calendar-new.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,
@@ -36,20 +57,8 @@ import {
safeJSONParse,
toArray,
} from "~/utils/zod";
import {
badRequestIfFalsy,
parseRequestFormData,
validate,
} from "~/utils/remix";
import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
} from "~/utils/dates";
import { calendarEventPage } from "~/utils/urls";
import { makeTitle } from "~/utils/strings";
import { i18next } from "~/modules/i18n";
import { canEditCalendarEvent } from "~/permissions";
import { DateInput } from "~/components/DateInput";
import { MapPoolSelector } from "./components/MapPoolSelector";
import { Tags } from "./components/Tags";
const MIN_DATE = new Date(Date.UTC(2015, 4, 28));
@@ -57,7 +66,10 @@ const MAX_DATE = new Date();
MAX_DATE.setFullYear(MAX_DATE.getFullYear() + 1);
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
return [
{ rel: "stylesheet", href: calendarNewStyles },
{ rel: "stylesheet", href: mapsStyles },
];
};
export const meta: MetaFunction = (args) => {
@@ -108,6 +120,7 @@ const newCalendarEventActionSchema = z.object({
processMany(safeJSONParse, removeDuplicates),
z.array(id).nullable()
),
pool: z.string().optional(),
});
export const action: ActionFunction = async ({ request }) => {
@@ -134,6 +147,16 @@ export const action: ActionFunction = async ({ request }) => {
: data.tags,
badges: data.badges ?? [],
};
const deserializedMaps = (() => {
if (!data.pool) return;
const mapPool = serializedStringToMapPool(data.pool);
return Object.entries(mapPool).flatMap(([mode, stages]) =>
stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId }))
);
})();
if (data.eventToEditId) {
const eventToEdit = badRequestIfFalsy(
db.calendarEvents.findById(data.eventToEditId)
@@ -142,6 +165,7 @@ export const action: ActionFunction = async ({ request }) => {
db.calendarEvents.update({
eventId: data.eventToEditId,
mapPoolMaps: deserializedMaps,
...commonArgs,
});
@@ -149,6 +173,7 @@ export const action: ActionFunction = async ({ request }) => {
} else {
const createdEventId = db.calendarEvents.create({
authorId: user.id,
mapPoolMaps: deserializedMaps,
...commonArgs,
});
@@ -181,6 +206,7 @@ export const loader = async ({ request }: LoaderArgs) => {
// "BADGE" tag is special and can't be edited like other tags
tags: eventToEdit.tags.filter((tag) => tag !== "BADGE"),
badges: db.calendarEvents.findBadgesByEventId(eventId),
mapPool: db.calendarEvents.findMapPoolByEventId(eventId),
}
: undefined,
title: makeTitle([canEditEvent ? "Edit" : "New", t("pages.calendar")]),
@@ -192,7 +218,7 @@ export default function CalendarNewEventPage() {
const { eventToEdit } = useLoaderData<typeof loader>();
return (
<Main halfWidth>
<Main className="calendar-new__container">
<Form className="stack md items-start" method="post">
{eventToEdit && (
<input
@@ -208,6 +234,7 @@ export default function CalendarNewEventPage() {
<DiscordLinkInput />
<TagsAdder />
<BadgesAdder />
<MapPoolSection />
<Button type="submit" className="mt-4" data-cy="submit-button">
{t("actions.submit")}
</Button>
@@ -497,3 +524,64 @@ function BadgesAdder() {
</div>
);
}
const DEFAULT_MAP_POOL = {
SZ: [],
TC: [],
CB: [],
RM: [],
TW: [],
};
function MapPoolSection() {
const { t } = useTranslation(["game-misc", "calendar"]);
const data = useLoaderData<typeof loader>();
const [mapPool, setMapPool] = React.useState<MapPool>(
data.eventToEdit?.mapPool ?? DEFAULT_MAP_POOL
);
const [includeMapPool, setIncludeMapPool] = React.useState(
Boolean(data.eventToEdit?.mapPool)
);
const handleMapPoolChange = ({
mode,
stageId,
}: {
mode: ModeShort;
stageId: StageId;
}) => {
const newMapPool = mapPool[mode].includes(stageId)
? {
...mapPool,
[mode]: mapPool[mode].filter((id) => id !== stageId),
}
: {
...mapPool,
[mode]: [...mapPool[mode], stageId],
};
setMapPool(newMapPool);
};
return (
<div className="w-full">
{includeMapPool && (
<input
type="hidden"
name="pool"
value={mapPoolToSerializedString(mapPool)}
/>
)}
<Label>{t("calendar:forms.mapPool")}</Label>
<div className="stack md">
<Toggle checked={includeMapPool} setChecked={setIncludeMapPool} tiny />
{includeMapPool && (
<MapPoolSelector
mapPool={mapPool}
handleMapPoolChange={handleMapPoolChange}
/>
)}
</div>
</div>
);
}

View File

@@ -20,6 +20,7 @@ import {
BUILDS_PAGE,
calendarEventPage,
CALENDAR_PAGE,
mapsPage,
navIconUrl,
plusSuggestionPage,
userPage,
@@ -85,6 +86,12 @@ export default function Index() {
description={t("front:badges.description")}
to={BADGES_PAGE}
/>
<FeatureCard
navItem="maps"
title={t("common:pages.maps")}
description={t("front:maps.description")}
to={mapsPage()}
/>
</div>
</div>
</Main>

View File

@@ -1,50 +1,47 @@
import type {
ActionFunction,
LinksFunction,
LoaderArgs,
} from "@remix-run/node";
import { redirect } from "@remix-run/node";
import type { LinksFunction, LoaderArgs } from "@remix-run/node";
import type { ShouldReloadFunction } from "@remix-run/react";
import { Link } from "@remix-run/react";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import clsx from "clsx";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useCopyToClipboard } from "react-use";
import invariant from "tiny-invariant";
import { Button } from "~/components/Button";
import { Image } from "~/components/Image";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { Toggle } from "~/components/Toggle";
import { db } from "~/db";
import {
modesShort,
modes,
stageIds,
type ModeShort,
type ModeWithStage,
type StageId,
} from "~/modules/in-game-lists";
import { modes, stageIds } from "~/modules/in-game-lists";
import type { MapPool } from "~/modules/map-pool-serializer/types";
import styles from "~/styles/maps.css";
import { mapsPage, modeImageUrl, stageImageUrl } from "~/utils/urls";
import clsx from "clsx";
import { Form, useLoaderData, useSearchParams } from "@remix-run/react";
import {
mapPoolToSerializedString,
serializedStringToMapPool,
} from "~/modules/map-pool-serializer";
import { requireUser, useUser } from "~/modules/auth";
import { ADMIN_DISCORD_ID, MAPS } from "~/constants";
import { Button } from "~/components/Button";
import { Input } from "~/components/Input";
import { Label } from "~/components/Label";
import { DownloadIcon } from "~/components/icons/Download";
import { Toggle } from "~/components/Toggle";
import {
generateMapList,
mapPoolToNonEmptyModes,
modesOrder,
} from "~/modules/map-list-generator";
import * as React from "react";
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";
import {
mapPoolToSerializedString,
serializedStringToMapPool,
} from "~/modules/map-pool-serializer";
import type { MapPool } from "~/modules/map-pool-serializer/types";
import styles from "~/styles/maps.css";
import {
calendarEventPage,
ipLabsMaps,
modeImageUrl,
stageImageUrl,
} from "~/utils/urls";
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
export const unstable_shouldReload: ShouldReloadFunction = () => false;
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
};
@@ -53,41 +50,25 @@ export const handle = {
i18n: "game-misc",
};
// 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(),
});
export const action: ActionFunction = async ({ request }) => {
const user = await requireUser(request);
const data = await parseRequestFormData({
request,
schema: mapsActionSchema,
});
const mapPool = serializedStringToMapPool(data.pool);
const maps = Object.entries(mapPool).flatMap(([mode, stages]) =>
stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId }))
);
db.maps.addMapPool({
ownerId: user.id,
code: data.code,
maps,
});
return redirect(mapsPage(data.code));
};
export const loader = ({ request }: LoaderArgs) => {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const calendarEventId = url.searchParams.get("eventId");
if (!code) return null;
const event = calendarEventId
? db.calendarEvents.findById(Number(calendarEventId))
: undefined;
return db.maps.findMapPoolByCode(code) ?? null;
return {
calendarEvent: event
? {
id: event.eventId,
name: event.name,
}
: undefined,
mapPool: event
? db.calendarEvents.findMapPoolByEventId(event.eventId)
: null,
};
};
const DEFAULT_MAP_POOL = {
@@ -99,23 +80,33 @@ const DEFAULT_MAP_POOL = {
};
export default function MapListPage() {
const user = useUser();
const { t } = useTranslation(["common"]);
const data = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
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} />
{data.calendarEvent && !searchParams.has("pool") && (
<div className="maps__pool-info">
{t("common:maps.mapPool")}:{" "}
<Link to={calendarEventPage(data.calendarEvent.id)}>
{data.calendarEvent.name}
</Link>
</div>
)}
<MapPoolSelector
mapPool={mapPool}
handleMapPoolChange={handleMapPoolChange}
/>
<a
href={ipLabsMaps(mapPoolToSerializedString(mapPool))}
target="_blank"
rel="noreferrer"
className="maps__tournament-map-list-link"
>
{t("common:maps.tournamentMaplist")}
</a>
<MapListCreator mapPool={mapPool} />
</Main>
);
@@ -126,20 +117,14 @@ function useSearchParamMapPool() {
const [searchParams, setSearchParams] = useSearchParams();
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")!);
}
if (data?.mapPool) {
return data.mapPool;
}
return DEFAULT_MAP_POOL;
})();
@@ -181,11 +166,8 @@ function MapPoolSelector({
mapPool: MapPool;
handleMapPoolChange: (args: { mode: ModeShort; stageId: StageId }) => void;
}) {
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["game-misc"]);
const editMode = !data;
return (
<div className="stack md">
{stageIds.map((stageId) => (
@@ -208,12 +190,10 @@ 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
@@ -236,69 +216,12 @@ 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) {
return true;
}
for (const stageId of mapPool[mode]) {
if (!(DEFAULT_MAP_POOL[mode] as StageId[]).includes(stageId)) {
return true;
}
}
}
return false;
})();
return (
<Form
className="maps__pool-loader-saver"
method={hasChanges ? "post" : "get"}
>
<input
type="hidden"
name="pool"
value={mapPoolToSerializedString(mapPool)}
/>
<div>
<Label>Code</Label>
<Input
name="code"
minLength={MAPS.CODE_MIN_LENGTH}
maxLength={MAPS.CODE_MAX_LENGTH}
/>
</div>
<Button
icon={hasChanges ? <UploadIcon /> : <DownloadIcon />}
variant="outlined"
type="submit"
>
{hasChanges ? "Save map pool" : "Load map pool"}
</Button>
</Form>
);
}
// xxx: crashes if only one map in mode
// xxx: presentational mode
function MapListCreator({ mapPool }: { mapPool: MapPool }) {
const { t } = useTranslation(["game-misc"]);
const { t } = useTranslation(["game-misc", "common"]);
const [mapList, setMapList] = React.useState<ModeWithStage[]>();
const [szEveryOther, setSzEveryOther] = React.useState(false);
const [, copyToClipboard] = useCopyToClipboard();
const handleCreateMaplist = () => {
const [list] = generateMapList(
@@ -318,19 +241,41 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
return (
<div className="maps__map-list-creator">
<div className="maps__toggle-container">
<Label>50% SZ</Label>
<Label>{t("common:maps.halfSz")}</Label>
<Toggle checked={szEveryOther} setChecked={setSzEveryOther} tiny />
</div>
<Button onClick={handleCreateMaplist}>Create map list</Button>
<Button onClick={handleCreateMaplist}>
{t("common:maps.createMapList")}
</Button>
{mapList && (
<ol className="maps__map-list">
{mapList.map(({ mode, stageId }, i) => (
<li key={i}>
{t(`game-misc:MODE_SHORT_${mode}`)}{" "}
{t(`game-misc:STAGE_${stageId}`)}
</li>
))}
</ol>
<>
<ol className="maps__map-list">
{mapList.map(({ mode, stageId }, i) => (
<li key={i}>
{t(`game-misc:MODE_SHORT_${mode}`)}{" "}
{t(`game-misc:STAGE_${stageId}`)}
</li>
))}
</ol>
<Button
tiny
variant="outlined"
onClick={() =>
copyToClipboard(
mapList
.map(
({ mode, stageId }, i) =>
`${i + 1}) ${t(`game-misc:MODE_SHORT_${mode}`)} ${t(
`game-misc:STAGE_${stageId}`
)}`
)
.join("\n")
)
}
>
{t("common:actions.copyToClipboard")}
</Button>
</>
)}
</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;

View File

@@ -1,3 +1,7 @@
.calendar-new__container {
max-width: 32rem;
}
.calendar-new__select {
max-width: 16rem;
}

View File

@@ -205,6 +205,10 @@ input[type="checkbox"] {
height: 16px;
}
[list]::-webkit-calendar-picker-indicator {
display: none !important;
}
label {
display: block;
font-size: var(--fonts-xs);

View File

@@ -2,11 +2,14 @@
max-width: 32rem;
}
.maps__pool-loader-saver {
display: flex;
align-items: flex-end;
justify-content: center;
gap: var(--s-4);
.maps__pool-info {
font-size: var(--fonts-xxs);
font-weight: var(--semi-bold);
}
.maps__tournament-map-list-link {
font-size: var(--fonts-xxs);
text-align: center;
}
.maps__stage-row {
@@ -18,6 +21,7 @@
.maps__stage-name-row {
display: flex;
flex-direction: column;
flex-grow: 1;
align-items: center;
justify-content: space-between;
@@ -25,11 +29,17 @@
border-radius: var(--rounded);
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
gap: var(--s-0-5);
gap: var(--s-2);
padding-block: var(--s-1-5);
padding-inline: var(--s-3);
}
@media screen and (min-width: 640px) {
.maps__stage-name-row {
flex-direction: row;
}
}
.maps__mode-buttons-container {
display: flex;
flex-wrap: wrap;
@@ -72,11 +82,6 @@
gap: var(--s-4);
}
.maps__pool_info {
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
}
.maps__toggle-container {
--label-margin: 0;
@@ -89,4 +94,5 @@
.maps__map-list {
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
margin-block-start: var(--s-4);
}

View File

@@ -1,5 +1,5 @@
import slugify from "slugify";
import type { Badge, GearType, MapPool, User } from "~/db/types";
import type { Badge, GearType, MapPoolMap, User } from "~/db/types";
import type { ModeShort, weaponCategories } from "~/modules/in-game-lists";
import type {
Ability,
@@ -23,6 +23,9 @@ export const GITHUB_CONTRIBUTORS_URL =
export const BORZOIC_TWITTER = "https://twitter.com/borzoic_";
export const LEAN_TWITTER = "https://twitter.com/LeanYoshi";
export const UBERU_TWITTER = "https://twitter.com/uberu5";
export const ipLabsMaps = (pool: string) =>
`https://maps.iplabs.ink/?3&pool=${pool}`;
export const LOG_IN_URL = "/auth";
export const LOG_OUT_URL = "/auth/logout";
export const ADMIN_PAGE = "/admin";
@@ -64,8 +67,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 mapsPage = (eventId?: MapPoolMap["calendarEventId"]) =>
`/maps${eventId ? `?eventId=${eventId}` : ""}`;
export const articlePage = (slug: string) => `/a/${slug}`;
export const analyzerPage = (args?: {
weaponId: MainWeaponId;

View File

@@ -0,0 +1,64 @@
module.exports.up = function (db) {
for (const table of ["MapPool", "MapPoolMap"]) {
db.prepare(`drop table "${table}"`).run();
}
db.prepare(`drop index calendar_event_map_pool_id`).run();
db.prepare(`alter table "CalendarEvent" drop column "mapPoolId"`).run();
// ---
db.prepare(
`
create table "MapPoolMap" (
"calendarEventId" integer,
"stageId" integer not null,
"mode" text not null,
foreign key ("calendarEventId") references "CalendarEvent"("id") on delete cascade,
unique("calendarEventId", "stageId", "mode") on conflict rollback
) strict
`
).run();
db.prepare(
`create index map_pool_map_calendar_event_id on "MapPoolMap"("calendarEventId")`
).run();
};
module.exports.down = function (db) {
db.prepare(`drop table "MapPoolMap"`).run();
// ---
db.prepare(
`
create table "MapPool" (
"id" integer primary key,
"code" text not null,
"ownerId" integer not null,
foreign key ("ownerId") references "User"("id") on delete restrict
) strict
`
).run();
db.prepare(`create index map_pool_owner_id on "MapPool"("ownerId")`).run();
db.prepare(
`
create table "MapPoolMap" (
"mapPoolId" integer not null,
"stageId" integer not null,
"mode" text not null,
foreign key ("mapPoolId") references "MapPool"("id") on delete cascade
) strict
`
).run();
db.prepare(
`create index map_pool_map_map_pool_id on "MapPoolMap"("mapPoolId")`
).run();
db.prepare(
`alter table "CalendarEvent" add "mapPoolId" integer references "MapPool"("id") on delete set null`
).run();
db.prepare(
`create index calendar_event_map_pool_id on "CalendarEvent"("mapPoolId")`
).run();
};

467
package-lock.json generated
View File

@@ -32,6 +32,7 @@
"react-flip-toolkit": "^7.0.16",
"react-i18next": "^11.18.6",
"react-popper": "^2.3.0",
"react-use": "^17.4.0",
"remix-auth": "^3.3.0",
"remix-auth-oauth2": "^1.3.0",
"remix-i18next": "^4.1.1",
@@ -2923,6 +2924,11 @@
"i18next": "^21.0.1"
}
},
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"node_modules/@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@@ -3386,6 +3392,11 @@
"resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz",
"integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="
},
"node_modules/@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yarnpkg/esbuild-plugin-pnp": {
"version": "3.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.11.tgz",
@@ -5025,6 +5036,14 @@
"node": ">=0.10.0"
}
},
"node_modules/copy-to-clipboard": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
"integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js-compat": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.23.1.tgz",
@@ -5140,6 +5159,35 @@
"node": ">=12.22"
}
},
"node_modules/css-in-js-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
"dependencies": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -5155,8 +5203,7 @@
"node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"dev": true
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
},
"node_modules/cypress": {
"version": "10.8.0",
@@ -5793,6 +5840,14 @@
"is-arrayish": "^0.2.1"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/es-abstract": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz",
@@ -7638,6 +7693,11 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"node_modules/fast-shallow-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
},
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@@ -7647,6 +7707,11 @@
"node": ">= 4.9.1"
}
},
"node_modules/fastest-stable-stringify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="
},
"node_modules/fastq": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
@@ -8602,6 +8667,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"node_modules/i18next": {
"version": "21.9.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.9.2.tgz",
@@ -8759,6 +8829,14 @@
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==",
"dev": true
},
"node_modules/inline-style-prefixer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz",
"integrity": "sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==",
"dependencies": {
"css-in-js-utils": "^2.0.0"
}
},
"node_modules/inquirer": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz",
@@ -9344,7 +9422,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9355,6 +9432,11 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"node_modules/js-sdsl": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
@@ -10262,6 +10344,11 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@@ -11212,6 +11299,25 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"node_modules/nano-css": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz",
"integrity": "sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==",
"dependencies": {
"css-tree": "^1.1.2",
"csstype": "^3.0.6",
"fastest-stable-stringify": "^2.0.2",
"inline-style-prefixer": "^6.0.0",
"rtl-css-js": "^1.14.0",
"sourcemap-codec": "^1.4.8",
"stacktrace-js": "^2.0.2",
"stylis": "^4.0.6"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -12443,6 +12549,40 @@
"react-dom": ">=16.8"
}
},
"node_modules/react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
"peerDependencies": {
"react": "*",
"tslib": "*"
}
},
"node_modules/react-use": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.4.0.tgz",
"integrity": "sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==",
"dependencies": {
"@types/js-cookie": "^2.2.6",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.3.1",
"react-universal-interface": "^0.6.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.1.0",
"set-harmonic-interval": "^1.0.1",
"throttle-debounce": "^3.0.1",
"ts-easing": "^0.2.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -12908,6 +13048,11 @@
"node": ">=0.10.5"
}
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
@@ -13039,6 +13184,14 @@
"estree-walker": "^0.6.1"
}
},
"node_modules/rtl-css-js": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.0.tgz",
"integrity": "sha512-Oc7PnzwIEU4M0K1J4h/7qUUaljXhQ0kCObRsZjxs2HjkpKsnoTMvSmvJ4sqgJZd0zBoEfAyTdnK/jMIYvrjySQ==",
"dependencies": {
"@babel/runtime": "^7.1.2"
}
},
"node_modules/run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@@ -13133,6 +13286,17 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@@ -13222,6 +13386,14 @@
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz",
"integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg=="
},
"node_modules/set-harmonic-interval": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==",
"engines": {
"node": ">=6.9"
}
},
"node_modules/set-value": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -13645,8 +13817,7 @@
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
},
"node_modules/space-separated-tokens": {
"version": "2.0.1",
@@ -13744,6 +13915,46 @@
"node": ">= 8"
}
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"node_modules/stacktrace-gps": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
"dependencies": {
"source-map": "0.5.6",
"stackframe": "^1.3.4"
}
},
"node_modules/stacktrace-gps/node_modules/source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stacktrace-js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
"dependencies": {
"error-stack-parser": "^2.0.6",
"stack-generator": "^2.0.5",
"stacktrace-gps": "^3.0.4"
}
},
"node_modules/static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@@ -14290,6 +14501,11 @@
"node": ">=10"
}
},
"node_modules/stylis": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.2.tgz",
"integrity": "sha512-Nn2CCrG2ZaFziDxaZPN43CXqn+j7tcdjPFCkRBkFue8QYXC2HdEwnw5TCBo4yQZ2WxKYeSi0fdoOrtEqgDrXbA=="
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -14500,6 +14716,14 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"node_modules/throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==",
"engines": {
"node": ">=10"
}
},
"node_modules/throttleit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
@@ -14599,6 +14823,11 @@
"node": ">=8.0"
}
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -14659,6 +14888,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ=="
},
"node_modules/ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -14737,8 +14971,7 @@
"node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/tsm": {
"version": "2.2.2",
@@ -17783,6 +18016,11 @@
"i18next": "^21.0.1"
}
},
"@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
@@ -18126,6 +18364,11 @@
"resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz",
"integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw=="
},
"@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"@yarnpkg/esbuild-plugin-pnp": {
"version": "3.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.11.tgz",
@@ -19346,6 +19589,14 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true
},
"copy-to-clipboard": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
"integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
"requires": {
"toggle-selection": "^1.0.6"
}
},
"core-js-compat": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.23.1.tgz",
@@ -19433,6 +19684,31 @@
"integrity": "sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==",
"dev": true
},
"css-in-js-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
"requires": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
}
},
"css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"requires": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -19442,8 +19718,7 @@
"csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"dev": true
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
},
"cypress": {
"version": "10.8.0",
@@ -19931,6 +20206,14 @@
"is-arrayish": "^0.2.1"
}
},
"error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"requires": {
"stackframe": "^1.3.4"
}
},
"es-abstract": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.0.tgz",
@@ -21221,12 +21504,22 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fast-shallow-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
},
"fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
"dev": true
},
"fastest-stable-stringify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="
},
"fastq": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz",
@@ -21961,6 +22254,11 @@
}
}
},
"hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"i18next": {
"version": "21.9.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-21.9.2.tgz",
@@ -22069,6 +22367,14 @@
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==",
"dev": true
},
"inline-style-prefixer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.1.tgz",
"integrity": "sha512-AsqazZ8KcRzJ9YPN1wMH2aNM7lkWQ8tSPrW5uDk1ziYwiAPWSZnUsC7lfZq+BDqLqz0B4Pho5wscWcJzVvRzDQ==",
"requires": {
"css-in-js-utils": "^2.0.0"
}
},
"inquirer": {
"version": "8.2.4",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz",
@@ -22478,8 +22784,7 @@
"isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
"isstream": {
"version": "0.1.2",
@@ -22487,6 +22792,11 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"js-sdsl": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
@@ -23202,6 +23512,11 @@
"integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==",
"dev": true
},
"mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@@ -23807,6 +24122,21 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"nano-css": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz",
"integrity": "sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==",
"requires": {
"css-tree": "^1.1.2",
"csstype": "^3.0.6",
"fastest-stable-stringify": "^2.0.2",
"inline-style-prefixer": "^6.0.0",
"rtl-css-js": "^1.14.0",
"sourcemap-codec": "^1.4.8",
"stacktrace-js": "^2.0.2",
"stylis": "^4.0.6"
}
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@@ -24707,6 +25037,33 @@
"react-router": "6.3.0"
}
},
"react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
"requires": {}
},
"react-use": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.4.0.tgz",
"integrity": "sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==",
"requires": {
"@types/js-cookie": "^2.2.6",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.3.1",
"react-universal-interface": "^0.6.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.1.0",
"set-harmonic-interval": "^1.0.1",
"throttle-debounce": "^3.0.1",
"ts-easing": "^0.2.0",
"tslib": "^2.1.0"
}
},
"read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -25068,6 +25425,11 @@
"integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
"dev": true
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
@@ -25172,6 +25534,14 @@
"estree-walker": "^0.6.1"
}
},
"rtl-css-js": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.0.tgz",
"integrity": "sha512-Oc7PnzwIEU4M0K1J4h/7qUUaljXhQ0kCObRsZjxs2HjkpKsnoTMvSmvJ4sqgJZd0zBoEfAyTdnK/jMIYvrjySQ==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@@ -25232,6 +25602,11 @@
"loose-envify": "^1.1.0"
}
},
"screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="
},
"section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
@@ -25307,6 +25682,11 @@
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz",
"integrity": "sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg=="
},
"set-harmonic-interval": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g=="
},
"set-value": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
@@ -25627,8 +26007,7 @@
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
},
"space-separated-tokens": {
"version": "2.0.1",
@@ -25708,6 +26087,45 @@
"minipass": "^3.1.1"
}
},
"stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"requires": {
"stackframe": "^1.3.4"
}
},
"stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"stacktrace-gps": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
"requires": {
"source-map": "0.5.6",
"stackframe": "^1.3.4"
},
"dependencies": {
"source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA=="
}
}
},
"stacktrace-js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
"requires": {
"error-stack-parser": "^2.0.6",
"stack-generator": "^2.0.5",
"stacktrace-gps": "^3.0.4"
}
},
"static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@@ -26123,6 +26541,11 @@
"postcss-sorting": "^7.0.1"
}
},
"stylis": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.2.tgz",
"integrity": "sha512-Nn2CCrG2ZaFziDxaZPN43CXqn+j7tcdjPFCkRBkFue8QYXC2HdEwnw5TCBo4yQZ2WxKYeSi0fdoOrtEqgDrXbA=="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -26301,6 +26724,11 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg=="
},
"throttleit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
@@ -26384,6 +26812,11 @@
"is-number": "^7.0.0"
}
},
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -26428,6 +26861,11 @@
"integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==",
"dev": true
},
"ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ=="
},
"ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -26479,8 +26917,7 @@
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"tsm": {
"version": "2.2.2",

View File

@@ -57,6 +57,7 @@
"react-flip-toolkit": "^7.0.16",
"react-i18next": "^11.18.6",
"react-popper": "^2.3.0",
"react-use": "^17.4.0",
"remix-auth": "^3.3.0",
"remix-auth-oauth2": "^1.3.0",
"remix-i18next": "^4.1.1",

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",
@@ -17,6 +18,7 @@
"forms.tags.info": "\"Badge prizes\" tag is added automatically if applicable",
"forms.badges": "Badge prizes",
"forms.badges.placeholder": "Choose a badge prize",
"forms.mapPool": "Map pool",
"forms.participantCount": "Participant count",
"forms.reportResultsHeader": "Reporting results for {{eventName}}",

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",
@@ -27,6 +28,12 @@
"actions.remove": "Remove",
"actions.delete": "Delete",
"actions.loadMore": "Load more",
"actions.copyToClipboard": "Copy to clipboard",
"maps.createMapList": "Create map list",
"maps.halfSz": "50% SZ",
"maps.mapPool": "Map pool",
"maps.tournamentMaplist": "Create tournament map list (maps.iplabs.ink)",
"results": "Results",

View File

@@ -6,6 +6,7 @@
"plus.description": "View Plus Server voting history and more",
"badges.description": "List of all the badges you can earn for your profile",
"analyzer.description": "Find out what your builds actually do",
"maps.description": "Turn pool of maps into a list to play on",
"recentWinners": "Recent winners",
"upcomingEvents": "Upcoming events",
"articleBy": "by {{author}}"

View File

@@ -23,18 +23,32 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**48/49**
**48/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.maps
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
</details>
@@ -46,13 +60,31 @@
**6/6**
### 🟢 front.json
### 🟡 front.json
**10/10**
**10/11**
### 🟢 game-misc.json
<details>
<summary>Missing</summary>
**12/12**
- maps.description
</details>
### 🟡 game-misc.json
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -78,7 +110,85 @@
---
## /de (🟢 Done)
## /de (🟡 In progress)
### 🟢 analyzer.json
**94/94**
### 🟢 badges.json
**7/7**
### 🟢 builds.json
**11/11**
### 🟡 calendar.json
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**49/55**
<details>
<summary>Missing</summary>
- pages.maps
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
</details>
### 🟢 contributions.json
**6/6**
### 🟢 faq.json
**6/6**
### 🟡 front.json
**10/11**
<details>
<summary>Missing</summary>
- maps.description
</details>
### 🟡 game-misc.json
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟢 user.json
**19/19**
---
@@ -105,18 +215,32 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**48/49**
**48/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.maps
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
</details>
@@ -128,13 +252,31 @@
**6/6**
### 🟢 front.json
### 🟡 front.json
**10/10**
**10/11**
### 🟢 game-misc.json
<details>
<summary>Missing</summary>
**12/12**
- maps.description
</details>
### 🟡 game-misc.json
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -183,20 +325,34 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**46/49**
**46/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.analyzer
- pages.maps
- actions.loadMore
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
</details>
@@ -218,19 +374,31 @@
### 🟡 front.json
**8/10**
**8/11**
<details>
<summary>Missing</summary>
- buildsGoTo
- analyzer.description
- maps.description
</details>
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -272,11 +440,11 @@
### 🔴 calendar.json
**0/44**
**0/46**
### 🔴 common.json
**0/49**
**0/55**
### 🔴 contributions.json
@@ -288,11 +456,22 @@
### 🔴 front.json
**0/10**
**0/11**
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🔴 user.json
@@ -402,20 +581,34 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**46/49**
**46/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.analyzer
- pages.maps
- actions.loadMore
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
</details>
@@ -436,19 +629,31 @@
### 🟡 front.json
**8/10**
**8/11**
<details>
<summary>Missing</summary>
- buildsGoTo
- analyzer.description
- maps.description
</details>
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -488,20 +693,34 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**35/49**
**35/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.analyzer
- pages.maps
- actions.loadMore
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
- weapon.category.SHOOTERS
- weapon.category.BLASTERS
- weapon.category.ROLLERS
@@ -533,19 +752,31 @@
### 🟡 front.json
**8/10**
**8/11**
<details>
<summary>Missing</summary>
- buildsGoTo
- analyzer.description
- maps.description
</details>
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -615,19 +846,33 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**36/49**
**36/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.maps
- actions.loadMore
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
- weapon.category.SHOOTERS
- weapon.category.BLASTERS
- weapon.category.ROLLERS
@@ -659,19 +904,31 @@
### 🟡 front.json
**8/10**
**8/11**
<details>
<summary>Missing</summary>
- buildsGoTo
- analyzer.description
- maps.description
</details>
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -711,20 +968,34 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**35/49**
**35/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.analyzer
- pages.maps
- actions.loadMore
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
- weapon.category.SHOOTERS
- weapon.category.BLASTERS
- weapon.category.ROLLERS
@@ -756,19 +1027,31 @@
### 🟡 front.json
**8/10**
**8/11**
<details>
<summary>Missing</summary>
- buildsGoTo
- analyzer.description
- maps.description
</details>
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json
@@ -808,20 +1091,34 @@
**11/11**
### 🟢 calendar.json
### 🟡 calendar.json
**44/44**
**44/46**
<details>
<summary>Missing</summary>
- createMapList
- forms.mapPool
</details>
### 🟡 common.json
**35/49**
**35/55**
<details>
<summary>Missing</summary>
- pages.s2
- pages.analyzer
- pages.maps
- actions.loadMore
- actions.copyToClipboard
- maps.createMapList
- maps.halfSz
- maps.mapPool
- maps.tournamentMaplist
- weapon.category.SHOOTERS
- weapon.category.BLASTERS
- weapon.category.ROLLERS
@@ -853,19 +1150,31 @@
### 🟡 front.json
**8/10**
**8/11**
<details>
<summary>Missing</summary>
- buildsGoTo
- analyzer.description
- maps.description
</details>
### 🟢 game-misc.json
### 🟡 game-misc.json
**12/12**
**12/17**
<details>
<summary>Missing</summary>
- MODE_SHORT_TW
- MODE_SHORT_SZ
- MODE_SHORT_TC
- MODE_SHORT_RM
- MODE_SHORT_CB
</details>
### 🟡 user.json