mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Logos for tournament pickups (#1768)
* Progress * Adjust when elements show up etc. * Reorganize * Can upload * Delete logo admin action * Filter out avas pre-start * Util func * Remove old code * Fixes
This commit is contained in:
parent
a7f77d9384
commit
2c5004f623
|
|
@ -5,6 +5,8 @@ import { useTranslation } from "react-i18next";
|
|||
import { Button, type ButtonProps } from "./Button";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { SubmitButton } from "./SubmitButton";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
export function FormWithConfirm({
|
||||
fields,
|
||||
|
|
@ -33,6 +35,7 @@ export function FormWithConfirm({
|
|||
const componentsFetcher = useFetcher();
|
||||
const fetcher = _fetcher ?? componentsFetcher;
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
const { t } = useTranslation(["common"]);
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
|
@ -51,17 +54,23 @@ export function FormWithConfirm({
|
|||
|
||||
return (
|
||||
<>
|
||||
<fetcher.Form
|
||||
id={id}
|
||||
className="hidden"
|
||||
ref={formRef}
|
||||
method="post"
|
||||
action={action}
|
||||
>
|
||||
{fields?.map(([name, value]) => (
|
||||
<input type="hidden" key={name} name={name} value={value} />
|
||||
))}
|
||||
</fetcher.Form>
|
||||
{isMounted
|
||||
? // using portal here makes nesting this component in another form work
|
||||
createPortal(
|
||||
<fetcher.Form
|
||||
id={id}
|
||||
className="hidden"
|
||||
ref={formRef}
|
||||
method="post"
|
||||
action={action}
|
||||
>
|
||||
{fields?.map(([name, value]) => (
|
||||
<input type="hidden" key={name} name={name} value={value} />
|
||||
))}
|
||||
</fetcher.Form>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
<Dialog isOpen={dialogOpen} close={closeDialog} className="text-center">
|
||||
<div className="stack md">
|
||||
<h2 className="text-sm">{dialogHeading}</h2>
|
||||
|
|
@ -82,6 +91,7 @@ export function FormWithConfirm({
|
|||
{React.cloneElement(children, {
|
||||
// @ts-expect-error broke with @types/react upgrade. TODO: figure out narrower type than React.ReactNode
|
||||
onClick: openDialog,
|
||||
type: "button",
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -606,6 +606,7 @@ export interface TournamentTeam {
|
|||
>;
|
||||
tournamentId: number;
|
||||
teamId: number | null;
|
||||
avatarImgId: number | null;
|
||||
}
|
||||
|
||||
export interface TournamentTeamCheckIn {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { parseParams } from "~/utils/remix";
|
|||
import { id } from "~/utils/zod";
|
||||
import type { GetTournamentTeamsResponse } from "../schema";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import {
|
||||
handleOptionsRequest,
|
||||
requireBearerAuth,
|
||||
} from "../api-public-utils.server";
|
||||
import i18next from "~/modules/i18n/i18next.server";
|
||||
import { cors } from "remix-utils/cors";
|
||||
import { userSubmittedImage } from "~/utils/urls";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
id,
|
||||
|
|
@ -29,6 +30,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
|
||||
const teams = await db
|
||||
.selectFrom("TournamentTeam")
|
||||
.leftJoin("UserSubmittedImage", "avatarImgId", "UserSubmittedImage.id")
|
||||
.leftJoin("TournamentTeamCheckIn", (join) =>
|
||||
join
|
||||
.onRef(
|
||||
|
|
@ -44,6 +46,22 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
"TournamentTeam.seed",
|
||||
"TournamentTeam.createdAt",
|
||||
"TournamentTeamCheckIn.checkedInAt",
|
||||
"UserSubmittedImage.url as avatarUrl",
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("AllTeam")
|
||||
.leftJoin(
|
||||
"UserSubmittedImage",
|
||||
"AllTeam.avatarImgId",
|
||||
"UserSubmittedImage.id",
|
||||
)
|
||||
.whereRef("AllTeam.id", "=", "TournamentTeam.teamId")
|
||||
.select([
|
||||
"AllTeam.customUrl",
|
||||
"UserSubmittedImage.url as logoUrl",
|
||||
"AllTeam.deletedAt",
|
||||
]),
|
||||
).as("team"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TournamentTeamMember")
|
||||
|
|
@ -74,11 +92,22 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
.orderBy("TournamentTeam.createdAt asc")
|
||||
.execute();
|
||||
|
||||
const logoUrl = (team: (typeof teams)[number]) => {
|
||||
const url = team.team?.logoUrl ?? team.avatarUrl;
|
||||
if (!url) return null;
|
||||
|
||||
return userSubmittedImage(url);
|
||||
};
|
||||
|
||||
const result: GetTournamentTeamsResponse = teams.map((team) => {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
url: `https://sendou.ink/to/${id}/teams/${team.id}`,
|
||||
teamPageUrl:
|
||||
team.team?.customUrl && !team.team.deletedAt
|
||||
? `https://sendou.ink/t/${team.team.customUrl}`
|
||||
: null,
|
||||
seed: team.seed,
|
||||
registeredAt: databaseTimestampToDate(team.createdAt).toISOString(),
|
||||
checkedIn: Boolean(team.checkedInAt),
|
||||
|
|
@ -94,6 +123,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
joinedAt: databaseTimestampToDate(member.createdAt).toISOString(),
|
||||
};
|
||||
}),
|
||||
logoUrl: logoUrl(team),
|
||||
mapPool:
|
||||
team.mapPool.length > 0
|
||||
? team.mapPool.map((map) => {
|
||||
|
|
|
|||
|
|
@ -82,9 +82,21 @@ export type GetTournamentTeamsResponse = Array<{
|
|||
registeredAt: string;
|
||||
checkedIn: boolean;
|
||||
/**
|
||||
* URL for the tournament team page.
|
||||
*
|
||||
* @example "https://sendou.ink/to/9/teams/327"
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* URL for the global team page.
|
||||
*
|
||||
* @example "https://sendou.ink/t/moonlight"
|
||||
*/
|
||||
teamPageUrl: string | null;
|
||||
/**
|
||||
* @example "https://sendou.nyc3.cdn.digitaloceanspaces.com/pickup-logo-uReSb1b1XS3TWGLCKMDUD-1719054364813.webp"
|
||||
*/
|
||||
logoUrl: string | null;
|
||||
seed: number | null;
|
||||
mapPool: Array<StageWithMode> | null;
|
||||
members: Array<{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import type { ActionFunction } from "@remix-run/node";
|
||||
import {
|
||||
redirect,
|
||||
unstable_composeUploadHandlers as composeUploadHandlers,
|
||||
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
|
||||
unstable_parseMultipartFormData as parseMultipartFormData,
|
||||
} from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import type { CalendarEventTag } from "~/db/types";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
|
|
@ -24,7 +19,12 @@ import {
|
|||
databaseTimestampToDate,
|
||||
dateToDatabaseTimestamp,
|
||||
} from "~/utils/dates";
|
||||
import { badRequestIfFalsy, parseFormData, validate } from "~/utils/remix";
|
||||
import {
|
||||
badRequestIfFalsy,
|
||||
parseFormData,
|
||||
uploadImageIfSubmitted,
|
||||
validate,
|
||||
} from "~/utils/remix";
|
||||
import { calendarEventPage } from "~/utils/urls";
|
||||
import {
|
||||
actualNumber,
|
||||
|
|
@ -38,21 +38,25 @@ import {
|
|||
safeJSONParse,
|
||||
toArray,
|
||||
} from "~/utils/zod";
|
||||
import { canAddNewEvent, regClosesAtDate } from "../calendar-utils";
|
||||
import { CALENDAR_EVENT, REG_CLOSES_AT_OPTIONS } from "../calendar-constants";
|
||||
import {
|
||||
calendarEventMaxDate,
|
||||
calendarEventMinDate,
|
||||
canAddNewEvent,
|
||||
regClosesAtDate,
|
||||
} from "../calendar-utils";
|
||||
import {
|
||||
canCreateTournament,
|
||||
formValuesToBracketProgression,
|
||||
} from "../calendar-utils.server";
|
||||
import { CALENDAR_EVENT, REG_CLOSES_AT_OPTIONS } from "../calendar-constants";
|
||||
import { calendarEventMaxDate, calendarEventMinDate } from "../calendar-utils";
|
||||
import { nanoid } from "nanoid";
|
||||
import { s3UploadHandler } from "~/features/img-upload";
|
||||
import invariant from "~/utils/invariant";
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const user = await requireUser(request);
|
||||
|
||||
const { avatarFileName, formData } = await uploadAvatarIfExists(request);
|
||||
const { avatarFileName, formData } = await uploadImageIfSubmitted({
|
||||
request,
|
||||
fileNamePrefix: "tournament-logo",
|
||||
});
|
||||
const data = await parseFormData({
|
||||
formData,
|
||||
schema: newCalendarEventActionSchema,
|
||||
|
|
@ -174,38 +178,6 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
}
|
||||
};
|
||||
|
||||
async function uploadAvatarIfExists(request: Request) {
|
||||
const uploadHandler = composeUploadHandlers(
|
||||
s3UploadHandler(`tournament-logo-${nanoid()}-${Date.now()}`),
|
||||
createMemoryUploadHandler(),
|
||||
);
|
||||
|
||||
try {
|
||||
const formData = await parseMultipartFormData(request, uploadHandler);
|
||||
const imgSrc = formData.get("img") as string | null;
|
||||
invariant(imgSrc);
|
||||
|
||||
const urlParts = imgSrc.split("/");
|
||||
const fileName = urlParts[urlParts.length - 1];
|
||||
invariant(fileName);
|
||||
|
||||
return {
|
||||
avatarFileName: fileName,
|
||||
formData,
|
||||
};
|
||||
} catch (err) {
|
||||
// user did not submit image
|
||||
if (err instanceof TypeError) {
|
||||
return {
|
||||
avatarFileName: undefined,
|
||||
formData: await request.formData(),
|
||||
};
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export const newCalendarEventActionSchema = z
|
||||
.object({
|
||||
eventToEditId: z.preprocess(actualNumber, id.nullish()),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ export function findByUserId(userId: number) {
|
|||
return db
|
||||
.selectFrom("TeamMember")
|
||||
.innerJoin("Team", "Team.id", "TeamMember.teamId")
|
||||
.select(["Team.id", "Team.customUrl", "Team.name"])
|
||||
.leftJoin("UserSubmittedImage", "UserSubmittedImage.id", "Team.avatarImgId")
|
||||
.select([
|
||||
"Team.id",
|
||||
"Team.customUrl",
|
||||
"Team.name",
|
||||
"UserSubmittedImage.url as logoUrl",
|
||||
])
|
||||
.where("TeamMember.userId", "=", userId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function MatchRosters({
|
|||
|
||||
return (
|
||||
<div className="tournament-bracket__rosters">
|
||||
<div>
|
||||
<div className="stack xxs">
|
||||
<div className="stack xs horizontal items-center text-lighter">
|
||||
<div className="tournament-bracket__team-one-dot" />
|
||||
Team 1
|
||||
|
|
@ -48,6 +48,8 @@ export function MatchRosters({
|
|||
<h2
|
||||
className={clsx("text-sm", {
|
||||
"text-lighter": !teamOne,
|
||||
"tournament-bracket__rosters__spaced-header":
|
||||
teamOneLogoSrc || teamTwoLogoSrc,
|
||||
})}
|
||||
>
|
||||
{teamOne ? (
|
||||
|
|
@ -59,7 +61,7 @@ export function MatchRosters({
|
|||
className="text-main-forced font-bold stack horizontal xs items-center"
|
||||
>
|
||||
{teamOneLogoSrc ? (
|
||||
<Avatar url={teamOneLogoSrc} size="xxs" />
|
||||
<Avatar url={teamOneLogoSrc} size="sm" />
|
||||
) : null}
|
||||
{teamOne.name}
|
||||
</Link>
|
||||
|
|
@ -92,12 +94,18 @@ export function MatchRosters({
|
|||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className="stack xxs">
|
||||
<div className="stack xs horizontal items-center text-lighter">
|
||||
<div className="tournament-bracket__team-two-dot" />
|
||||
Team 2
|
||||
</div>
|
||||
<h2 className={clsx("text-sm", { "text-lighter": !teamTwo })}>
|
||||
<h2
|
||||
className={clsx("text-sm", {
|
||||
"text-lighter": !teamTwo,
|
||||
"tournament-bracket__rosters__spaced-header":
|
||||
teamOneLogoSrc || teamTwoLogoSrc,
|
||||
})}
|
||||
>
|
||||
{teamTwo ? (
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
|
|
@ -107,7 +115,7 @@ export function MatchRosters({
|
|||
className="text-main-forced font-bold stack horizontal xs items-center"
|
||||
>
|
||||
{teamTwoLogoSrc ? (
|
||||
<Avatar url={teamTwoLogoSrc} size="xxs" />
|
||||
<Avatar url={teamTwoLogoSrc} size="sm" />
|
||||
) : null}
|
||||
{teamTwo.name}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { notFoundIfFalsy } from "~/utils/remix";
|
|||
import type { Unwrapped } from "~/utils/types";
|
||||
import { getServerTournamentManager } from "./brackets-manager/manager.server";
|
||||
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
|
||||
import { isAdmin } from "~/permissions";
|
||||
|
||||
const manager = getServerTournamentManager();
|
||||
|
||||
|
|
@ -35,12 +36,14 @@ function dataMapped({
|
|||
ctx: TournamentRepository.FindById;
|
||||
user?: { id: number };
|
||||
}) {
|
||||
const revealAllMapPools =
|
||||
data.stage.length > 0 ||
|
||||
const tournamentHasStarted = data.stage.length > 0;
|
||||
const isOrganizer =
|
||||
ctx.author.id === user?.id ||
|
||||
ctx.staff.some(
|
||||
(staff) => staff.id === user?.id && staff.role === "ORGANIZER",
|
||||
);
|
||||
) ||
|
||||
isAdmin(user);
|
||||
const revealInfo = tournamentHasStarted || isOrganizer;
|
||||
|
||||
return {
|
||||
data,
|
||||
|
|
@ -53,7 +56,9 @@ function dataMapped({
|
|||
|
||||
return {
|
||||
...team,
|
||||
mapPool: revealAllMapPools || isOwnTeam ? team.mapPool : null,
|
||||
mapPool: revealInfo || isOwnTeam ? team.mapPool : null,
|
||||
pickupAvatarUrl:
|
||||
revealInfo || isOwnTeam ? team.pickupAvatarUrl : null,
|
||||
inviteCode: isOwnTeam ? team.inviteCode : null,
|
||||
};
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -488,9 +488,11 @@ export class Tournament {
|
|||
}
|
||||
|
||||
tournamentTeamLogoSrc(team: TournamentDataTeam) {
|
||||
if (!team.team?.logoUrl) return;
|
||||
const url = team.team?.logoUrl ?? team.pickupAvatarUrl;
|
||||
|
||||
return userSubmittedImage(team.team.logoUrl);
|
||||
if (!url) return;
|
||||
|
||||
return userSubmittedImage(url);
|
||||
}
|
||||
|
||||
resolvePoolCode({
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const createTeam = (teamId: number, userIds: number[]): TournamentDataTeam => ({
|
|||
team: null,
|
||||
seed: 1,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
});
|
||||
|
||||
function summarize({ results }: { results?: AllMatchResult[] } = {}) {
|
||||
|
|
|
|||
|
|
@ -2097,6 +2097,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709743534,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2207,6 +2208,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709737918,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2330,6 +2332,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709743523,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2440,6 +2443,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709743262,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2550,6 +2554,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709741396,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2673,6 +2678,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709711811,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2796,6 +2802,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709738831,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -2919,6 +2926,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709737837,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3029,6 +3037,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709741719,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3165,6 +3174,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709730354,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3288,6 +3298,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709745630,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3408,6 +3419,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709592381,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3544,6 +3556,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709723749,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3667,6 +3680,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709668399,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3803,6 +3817,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709735267,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -3917,6 +3932,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709745849,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4031,6 +4047,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709742258,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4151,6 +4168,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709738744,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4274,6 +4292,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709746054,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4385,6 +4404,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709744894,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4495,6 +4515,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709728278,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4605,6 +4626,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709715006,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4715,6 +4737,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709660578,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4829,6 +4852,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709721869,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -4953,6 +4977,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709743633,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5067,6 +5092,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709738747,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5194,6 +5220,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709626047,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5317,6 +5344,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709727951,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5427,6 +5455,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709741482,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5560,6 +5589,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709744451,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5674,6 +5704,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709726536,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5784,6 +5815,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709558706,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -5898,6 +5930,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709744323,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -6025,6 +6058,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709677397,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -6135,6 +6169,7 @@ export const PADDLING_POOL_257 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1709618711,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -7962,6 +7997,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708476597,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8072,6 +8108,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708535137,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8182,6 +8219,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708533764,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8302,6 +8340,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708537512,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8425,6 +8464,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708533309,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8535,6 +8575,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708430641,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8658,6 +8699,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708536306,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8765,6 +8807,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708526368,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -8875,6 +8918,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708506060,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9011,6 +9055,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708526814,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9118,6 +9163,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708466421,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9241,6 +9287,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708377426,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9361,6 +9408,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708448289,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9497,6 +9545,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708532602,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9607,6 +9656,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708535205,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9730,6 +9780,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708515945,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9840,6 +9891,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708453334,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -9947,6 +9999,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708522730,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10070,6 +10123,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708375443,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10193,6 +10247,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708532665,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10307,6 +10362,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708364254,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10434,6 +10490,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708464101,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10558,6 +10615,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708520249,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10681,6 +10739,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708535804,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10791,6 +10850,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708535891,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -10914,6 +10974,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708521749,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11037,6 +11098,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708536584,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11147,6 +11209,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708537772,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11287,6 +11350,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708379916,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11410,6 +11474,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708519753,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11537,6 +11602,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708534312,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11661,6 +11727,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708531929,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11771,6 +11838,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708477155,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -11885,6 +11953,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708531564,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -12038,6 +12107,7 @@ export const PADDLING_POOL_255 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1708503356,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14142,6 +14212,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707443313,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14240,6 +14311,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707366405,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14338,6 +14410,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1706912643,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14436,6 +14509,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707359335,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14560,6 +14634,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707171426,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14671,6 +14746,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707342696,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14782,6 +14858,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707513942,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -14906,6 +14983,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707526815,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15030,6 +15108,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707583385,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15128,6 +15207,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707486395,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15226,6 +15306,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707513290,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15324,6 +15405,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707531084,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15422,6 +15504,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707568466,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15533,6 +15616,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707481625,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15631,6 +15715,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707530166,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15729,6 +15814,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707181792,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15840,6 +15926,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707550321,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -15955,6 +16042,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707575096,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16066,6 +16154,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707569490,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16190,6 +16279,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707537425,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16288,6 +16378,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707564691,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16416,6 +16507,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707145818,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16522,6 +16614,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707558330,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16620,6 +16713,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707586842,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16718,6 +16812,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707583597,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16842,6 +16937,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707429804,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -16953,6 +17049,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707539973,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17068,6 +17165,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707507831,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17179,6 +17277,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707586297,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17287,6 +17386,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707583885,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17411,6 +17511,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707578076,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17523,6 +17624,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
prefersNotToHost: 1,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707582953,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17621,6 +17723,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707575330,
|
||||
members: [
|
||||
{
|
||||
|
|
@ -17732,6 +17835,7 @@ export const IN_THE_ZONE_32 = () =>
|
|||
team: null,
|
||||
inviteCode: null,
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
createdAt: 1707527645,
|
||||
members: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const tournamentCtxTeam = (
|
|||
mapPool: [],
|
||||
members: [],
|
||||
activeRosterUserIds: [],
|
||||
pickupAvatarUrl: null,
|
||||
name: "Team " + teamId,
|
||||
prefersNotToHost: 0,
|
||||
droppedOut: 0,
|
||||
|
|
|
|||
|
|
@ -311,6 +311,12 @@
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
.tournament-bracket__rosters__spaced-header {
|
||||
min-height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.tournament-bracket__rosters {
|
||||
justify-content: space-evenly;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ export async function findById(id: number) {
|
|||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TournamentTeam")
|
||||
.leftJoin(
|
||||
"UserSubmittedImage",
|
||||
"TournamentTeam.avatarImgId",
|
||||
"UserSubmittedImage.id",
|
||||
)
|
||||
.select(({ eb: innerEb }) => [
|
||||
"TournamentTeam.id",
|
||||
"TournamentTeam.name",
|
||||
|
|
@ -88,6 +93,7 @@ export async function findById(id: number) {
|
|||
"TournamentTeam.inviteCode",
|
||||
"TournamentTeam.createdAt",
|
||||
"TournamentTeam.activeRosterUserIds",
|
||||
"UserSubmittedImage.url as pickupAvatarUrl",
|
||||
jsonArrayFrom(
|
||||
innerEb
|
||||
.selectFrom("TournamentTeamMember")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// TODO: add rest of the functions here that relate more to tournament teams than tournament/bracket
|
||||
|
||||
import type { Transaction } from "kysely";
|
||||
import { sql } from "kysely";
|
||||
import { nanoid } from "nanoid";
|
||||
import { INVITE_CODE_LENGTH } from "~/constants";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
|
||||
export function setActiveRoster({
|
||||
|
|
@ -85,3 +89,123 @@ export async function updateMemberInGameNameForNonStarted({
|
|||
.execute()
|
||||
);
|
||||
}
|
||||
|
||||
export function create({
|
||||
team,
|
||||
avatarFileName,
|
||||
userId,
|
||||
tournamentId,
|
||||
ownerInGameName,
|
||||
}: {
|
||||
team: Pick<
|
||||
Tables["TournamentTeam"],
|
||||
"name" | "prefersNotToHost" | "noScreen" | "teamId"
|
||||
>;
|
||||
avatarFileName?: string;
|
||||
userId: number;
|
||||
tournamentId: number;
|
||||
ownerInGameName: string | null;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const avatarImgId = avatarFileName
|
||||
? await createSubmittedImageInTrx({
|
||||
trx,
|
||||
avatarFileName,
|
||||
userId,
|
||||
})
|
||||
: null;
|
||||
|
||||
const tournamentTeam = await trx
|
||||
.insertInto("TournamentTeam")
|
||||
.values({
|
||||
tournamentId,
|
||||
name: team.name,
|
||||
inviteCode: nanoid(INVITE_CODE_LENGTH),
|
||||
prefersNotToHost: team.prefersNotToHost,
|
||||
noScreen: team.noScreen,
|
||||
teamId: team.teamId,
|
||||
avatarImgId,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await trx
|
||||
.insertInto("TournamentTeamMember")
|
||||
.values({
|
||||
tournamentTeamId: tournamentTeam.id,
|
||||
userId,
|
||||
isOwner: 1,
|
||||
inGameName: ownerInGameName,
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
export function update({
|
||||
team,
|
||||
avatarFileName,
|
||||
userId,
|
||||
}: {
|
||||
team: Pick<
|
||||
Tables["TournamentTeam"],
|
||||
"id" | "name" | "prefersNotToHost" | "noScreen" | "teamId"
|
||||
>;
|
||||
avatarFileName?: string;
|
||||
userId: number;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const avatarImgId = avatarFileName
|
||||
? await createSubmittedImageInTrx({
|
||||
trx,
|
||||
avatarFileName,
|
||||
userId,
|
||||
})
|
||||
: // don't overwrite the existing avatarImgId even if no new avatar is provided
|
||||
// delete is a separate functionality
|
||||
undefined;
|
||||
|
||||
await trx
|
||||
.updateTable("TournamentTeam")
|
||||
.set({
|
||||
name: team.name,
|
||||
prefersNotToHost: team.prefersNotToHost,
|
||||
noScreen: team.noScreen,
|
||||
teamId: team.teamId,
|
||||
avatarImgId,
|
||||
})
|
||||
.where("TournamentTeam.id", "=", team.id)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
async function createSubmittedImageInTrx({
|
||||
trx,
|
||||
avatarFileName,
|
||||
userId,
|
||||
}: {
|
||||
trx: Transaction<DB>;
|
||||
avatarFileName: string;
|
||||
userId: number;
|
||||
}) {
|
||||
const result = await trx
|
||||
.insertInto("UnvalidatedUserSubmittedImage")
|
||||
.values({
|
||||
url: avatarFileName,
|
||||
// in the context of tournament teams images are treated as globally "validated"
|
||||
// instead the TO takes responsibility for removing inappropriate images
|
||||
validatedAt: databaseTimestampNow(),
|
||||
submitterUserId: userId,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return result.id;
|
||||
}
|
||||
|
||||
export function deleteLogo(tournamentTeamId: number) {
|
||||
return db
|
||||
.updateTable("TournamentTeam")
|
||||
.set({ avatarImgId: null })
|
||||
.where("TournamentTeam.id", "=", tournamentTeamId)
|
||||
.execute();
|
||||
}
|
||||
|
|
|
|||
246
app/features/tournament/actions/to.$id.register.server.ts
Normal file
246
app/features/tournament/actions/to.$id.register.server.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import { type ActionFunction } from "@remix-run/node";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import * as QRepository from "~/features/sendouq/QRepository.server";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import {
|
||||
clearTournamentDataCache,
|
||||
tournamentFromDB,
|
||||
} from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { logger } from "~/utils/logger";
|
||||
import {
|
||||
notFoundIfFalsy,
|
||||
parseFormData,
|
||||
uploadImageIfSubmitted,
|
||||
validate,
|
||||
} from "~/utils/remix";
|
||||
import { booleanToInt } from "~/utils/sql";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { checkIn } from "../queries/checkIn.server";
|
||||
import { deleteTeam } from "../queries/deleteTeam.server";
|
||||
import deleteTeamMember from "../queries/deleteTeamMember.server";
|
||||
import { findByIdentifier } from "../queries/findByIdentifier.server";
|
||||
import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server";
|
||||
import { joinTeam } from "../queries/joinLeaveTeam.server";
|
||||
import { upsertCounterpickMaps } from "../queries/upsertCounterpickMaps.server";
|
||||
import { TOURNAMENT } from "../tournament-constants";
|
||||
import { registerSchema } from "../tournament-schemas.server";
|
||||
import {
|
||||
isOneModeTournamentOf,
|
||||
tournamentIdFromParams,
|
||||
validateCounterPickMapPool,
|
||||
} from "../tournament-utils";
|
||||
import { inGameNameIfNeeded } from "../tournament-utils.server";
|
||||
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const user = await requireUser(request);
|
||||
const { avatarFileName, formData } = await uploadImageIfSubmitted({
|
||||
request,
|
||||
fileNamePrefix: "pickup-logo",
|
||||
});
|
||||
const data = await parseFormData({
|
||||
formData,
|
||||
schema: registerSchema,
|
||||
});
|
||||
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const tournament = await tournamentFromDB({ tournamentId, user });
|
||||
const event = notFoundIfFalsy(findByIdentifier(tournamentId));
|
||||
|
||||
validate(
|
||||
!tournament.hasStarted,
|
||||
"Tournament has started, cannot make edits to registration",
|
||||
);
|
||||
|
||||
const ownTeam = tournament.ownedTeamByUser(user);
|
||||
const ownTeamCheckedIn = Boolean(ownTeam && ownTeam.checkIns.length > 0);
|
||||
|
||||
switch (data._action) {
|
||||
case "UPSERT_TEAM": {
|
||||
validate(
|
||||
!data.teamId ||
|
||||
(await TeamRepository.findByUserId(user.id))?.id === data.teamId,
|
||||
"Team id does not match the team you are in",
|
||||
);
|
||||
|
||||
if (ownTeam) {
|
||||
validate(
|
||||
tournament.registrationOpen || data.teamName === ownTeam.name,
|
||||
"Can't change team name after registration has closed",
|
||||
);
|
||||
|
||||
await TournamentTeamRepository.update({
|
||||
userId: user.id,
|
||||
avatarFileName,
|
||||
team: {
|
||||
id: ownTeam.id,
|
||||
name: data.teamName,
|
||||
prefersNotToHost: booleanToInt(data.prefersNotToHost),
|
||||
noScreen: booleanToInt(data.noScreen),
|
||||
teamId: data.teamId ?? null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
validate(!tournament.isInvitational, "Event is invite only");
|
||||
validate(
|
||||
(await UserRepository.findLeanById(user.id))?.friendCode,
|
||||
"No friend code",
|
||||
);
|
||||
validate(
|
||||
!tournament.teamMemberOfByUser(user),
|
||||
"You are already in a team that you aren't captain of",
|
||||
);
|
||||
validate(tournament.registrationOpen, "Registration is closed");
|
||||
|
||||
await TournamentTeamRepository.create({
|
||||
ownerInGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: user.id,
|
||||
}),
|
||||
team: {
|
||||
name: data.teamName,
|
||||
noScreen: booleanToInt(data.noScreen),
|
||||
prefersNotToHost: booleanToInt(data.prefersNotToHost),
|
||||
teamId: data.teamId ?? null,
|
||||
},
|
||||
userId: user.id,
|
||||
tournamentId,
|
||||
avatarFileName,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "DELETE_TEAM_MEMBER": {
|
||||
validate(ownTeam);
|
||||
validate(ownTeam.members.some((member) => member.userId === data.userId));
|
||||
validate(data.userId !== user.id);
|
||||
|
||||
const detailedOwnTeam = findOwnTournamentTeam({
|
||||
tournamentId,
|
||||
userId: user.id,
|
||||
});
|
||||
// making sure they aren't unfilling one checking in condition i.e. having full roster
|
||||
// and then having members kicked without it affecting the checking in status
|
||||
validate(
|
||||
detailedOwnTeam &&
|
||||
(!detailedOwnTeam.checkedInAt ||
|
||||
ownTeam.members.length > TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL),
|
||||
);
|
||||
|
||||
deleteTeamMember({ tournamentTeamId: ownTeam.id, userId: data.userId });
|
||||
break;
|
||||
}
|
||||
case "LEAVE_TEAM": {
|
||||
validate(!ownTeam, "Can't leave a team as the owner");
|
||||
|
||||
const teamMemberOf = tournament.teamMemberOfByUser(user);
|
||||
validate(teamMemberOf, "You are not in a team");
|
||||
validate(
|
||||
teamMemberOf.checkIns.length === 0,
|
||||
"You cannot leave after checking in",
|
||||
);
|
||||
|
||||
deleteTeamMember({
|
||||
tournamentTeamId: teamMemberOf.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "UPDATE_MAP_POOL": {
|
||||
const mapPool = new MapPool(data.mapPool);
|
||||
validate(ownTeam);
|
||||
validate(
|
||||
validateCounterPickMapPool(
|
||||
mapPool,
|
||||
isOneModeTournamentOf(event),
|
||||
tournament.ctx.tieBreakerMapPool,
|
||||
) === "VALID",
|
||||
);
|
||||
|
||||
upsertCounterpickMaps({
|
||||
tournamentTeamId: ownTeam.id,
|
||||
mapPool: new MapPool(data.mapPool),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "CHECK_IN": {
|
||||
logger.info(
|
||||
`Checking in (try): owned tournament team id: ${ownTeam?.id} - user id: ${user.id} - tournament id: ${tournamentId}`,
|
||||
);
|
||||
|
||||
const teamMemberOf = tournament.teamMemberOfByUser(user);
|
||||
validate(teamMemberOf, "You are not in a team");
|
||||
validate(
|
||||
teamMemberOf.checkIns.length === 0,
|
||||
"You have already checked in",
|
||||
);
|
||||
|
||||
validate(tournament.regularCheckInIsOpen, "Check in is not open");
|
||||
validate(
|
||||
tournament.checkInConditionsFulfilledByTeamId(teamMemberOf.id),
|
||||
"Check in conditions not fulfilled",
|
||||
);
|
||||
|
||||
checkIn(teamMemberOf.id);
|
||||
logger.info(
|
||||
`Checking in (success): tournament team id: ${teamMemberOf.id} - user id: ${user.id} - tournament id: ${tournamentId}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "ADD_PLAYER": {
|
||||
validate(
|
||||
tournament.ctx.teams.every((team) =>
|
||||
team.members.every((member) => member.userId !== data.userId),
|
||||
),
|
||||
"User is already in a team",
|
||||
);
|
||||
validate(ownTeam);
|
||||
validate(
|
||||
(await QRepository.usersThatTrusted(user.id)).some(
|
||||
(trusterPlayer) => trusterPlayer.id === data.userId,
|
||||
),
|
||||
"No trust given from this user",
|
||||
);
|
||||
validate(
|
||||
(await UserRepository.findLeanById(user.id))?.friendCode,
|
||||
"No friend code",
|
||||
);
|
||||
validate(tournament.registrationOpen, "Registration is closed");
|
||||
|
||||
joinTeam({
|
||||
userId: data.userId,
|
||||
newTeamId: ownTeam.id,
|
||||
tournamentId,
|
||||
inGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: data.userId,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UNREGISTER": {
|
||||
validate(ownTeam, "You are not registered to this tournament");
|
||||
validate(!ownTeamCheckedIn, "You cannot unregister after checking in");
|
||||
|
||||
deleteTeam(ownTeam.id);
|
||||
break;
|
||||
}
|
||||
case "DELETE_LOGO": {
|
||||
validate(ownTeam, "You are not registered to this tournament");
|
||||
|
||||
await TournamentTeamRepository.deleteLogo(ownTeam.id);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
}
|
||||
|
||||
clearTournamentDataCache(tournamentId);
|
||||
|
||||
return null;
|
||||
};
|
||||
31
app/features/tournament/loaders/to.$id.register.server.ts
Normal file
31
app/features/tournament/loaders/to.$id.register.server.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import * as QRepository from "~/features/sendouq/QRepository.server";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server";
|
||||
import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server";
|
||||
import { tournamentIdFromParams } from "../tournament-utils";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await getUser(request);
|
||||
if (!user) return null;
|
||||
|
||||
const ownTournamentTeam = findOwnTournamentTeam({
|
||||
tournamentId: tournamentIdFromParams(params),
|
||||
userId: user.id,
|
||||
});
|
||||
if (!ownTournamentTeam)
|
||||
return {
|
||||
mapPool: null,
|
||||
trusterPlayers: null,
|
||||
team: await TeamRepository.findByUserId(user.id),
|
||||
};
|
||||
|
||||
return {
|
||||
mapPool: findMapPoolByTeamId(ownTournamentTeam.id),
|
||||
trusterPlayers: await QRepository.usersThatTrusted(user.id),
|
||||
team: await TeamRepository.findByUserId(user.id),
|
||||
};
|
||||
};
|
||||
|
||||
export type TournamentRegisterPageLoader = typeof loader;
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { TournamentTeam, User } from "~/db/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { INVITE_CODE_LENGTH } from "~/constants";
|
||||
|
||||
const createTeamStm = sql.prepare(/*sql*/ `
|
||||
insert into "TournamentTeam" (
|
||||
"tournamentId",
|
||||
"inviteCode",
|
||||
"name",
|
||||
"prefersNotToHost",
|
||||
"noScreen",
|
||||
"teamId"
|
||||
) values (
|
||||
@tournamentId,
|
||||
@inviteCode,
|
||||
@name,
|
||||
@prefersNotToHost,
|
||||
@noScreen,
|
||||
@teamId
|
||||
) returning *
|
||||
`);
|
||||
|
||||
const createMemberStm = sql.prepare(/*sql*/ `
|
||||
insert into "TournamentTeamMember" (
|
||||
"tournamentTeamId",
|
||||
"userId",
|
||||
"inGameName",
|
||||
"isOwner"
|
||||
) values (
|
||||
@tournamentTeamId,
|
||||
@userId,
|
||||
@inGameName,
|
||||
1
|
||||
)
|
||||
`);
|
||||
|
||||
export const createTeam = sql.transaction(
|
||||
({
|
||||
tournamentId,
|
||||
name,
|
||||
ownerId,
|
||||
ownerInGameName,
|
||||
prefersNotToHost,
|
||||
noScreen,
|
||||
teamId,
|
||||
}: {
|
||||
tournamentId: TournamentTeam["tournamentId"];
|
||||
name: TournamentTeam["name"];
|
||||
ownerId: User["id"];
|
||||
ownerInGameName: string | null;
|
||||
prefersNotToHost: TournamentTeam["prefersNotToHost"];
|
||||
noScreen: number;
|
||||
teamId: number | null;
|
||||
}) => {
|
||||
const team = createTeamStm.get({
|
||||
tournamentId,
|
||||
name,
|
||||
inviteCode: nanoid(INVITE_CODE_LENGTH),
|
||||
prefersNotToHost,
|
||||
noScreen,
|
||||
teamId,
|
||||
}) as TournamentTeam;
|
||||
|
||||
createMemberStm.run({
|
||||
tournamentTeamId: team.id,
|
||||
inGameName: ownerInGameName,
|
||||
userId: ownerId,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { TournamentTeam } from "~/db/types";
|
||||
|
||||
const stm = sql.prepare(/*sql*/ `
|
||||
update
|
||||
"TournamentTeam"
|
||||
set
|
||||
"name" = @name,
|
||||
"prefersNotToHost" = @prefersNotToHost,
|
||||
"noScreen" = @noScreen,
|
||||
"teamId" = @teamId
|
||||
where
|
||||
"id" = @id
|
||||
`);
|
||||
|
||||
export function updateTeamInfo({
|
||||
id,
|
||||
name,
|
||||
prefersNotToHost,
|
||||
noScreen,
|
||||
teamId,
|
||||
}: {
|
||||
id: TournamentTeam["id"];
|
||||
name: TournamentTeam["name"];
|
||||
prefersNotToHost: TournamentTeam["prefersNotToHost"];
|
||||
noScreen: number;
|
||||
teamId: number | null;
|
||||
}) {
|
||||
stm.run({
|
||||
id,
|
||||
name,
|
||||
prefersNotToHost,
|
||||
noScreen,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
|
|
@ -39,7 +39,6 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import * as TournamentRepository from "../TournamentRepository.server";
|
||||
import { changeTeamOwner } from "../queries/changeTeamOwner.server";
|
||||
import { createTeam } from "../queries/createTeam.server";
|
||||
import { deleteTeam } from "../queries/deleteTeam.server";
|
||||
import { joinTeam, leaveTeam } from "../queries/joinLeaveTeam.server";
|
||||
import { adminActionSchema } from "../tournament-schemas.server";
|
||||
|
|
@ -74,17 +73,19 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
"User already on a team",
|
||||
);
|
||||
|
||||
createTeam({
|
||||
name: data.teamName,
|
||||
teamId: null,
|
||||
tournamentId: tournamentId,
|
||||
ownerId: data.userId,
|
||||
prefersNotToHost: 0,
|
||||
noScreen: 0,
|
||||
await TournamentTeamRepository.create({
|
||||
ownerInGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: data.userId,
|
||||
userId: user.id,
|
||||
}),
|
||||
team: {
|
||||
name: data.teamName,
|
||||
noScreen: 0,
|
||||
prefersNotToHost: 0,
|
||||
teamId: null,
|
||||
},
|
||||
userId: user.id,
|
||||
tournamentId,
|
||||
});
|
||||
|
||||
break;
|
||||
|
|
@ -316,6 +317,13 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case "DELETE_LOGO": {
|
||||
validateIsTournamentOrganizer();
|
||||
|
||||
await TournamentTeamRepository.deleteLogo(data.teamId);
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
|
|
@ -448,6 +456,11 @@ const actions = [
|
|||
inputs: ["ROSTER_MEMBER", "REGISTERED_TEAM", "IN_GAME_NAME"] as Input[],
|
||||
when: ["IN_GAME_NAME_REQUIRED"],
|
||||
},
|
||||
{
|
||||
type: "DELETE_LOGO",
|
||||
inputs: ["REGISTERED_TEAM"] as Input[],
|
||||
when: [],
|
||||
},
|
||||
] as const;
|
||||
|
||||
function TeamActions() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Link, useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import { Form, Link, useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import Compressor from "compressorjs";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
|
|
@ -15,36 +15,27 @@ import { Image, ModeImage } from "~/components/Image";
|
|||
import { Input } from "~/components/Input";
|
||||
import { Label } from "~/components/Label";
|
||||
import { MapPoolStages } from "~/components/MapPoolSelector";
|
||||
import { NewTabs } from "~/components/NewTabs";
|
||||
import { Popover } from "~/components/Popover";
|
||||
import { Section } from "~/components/Section";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { CheckmarkIcon } from "~/components/icons/Checkmark";
|
||||
import { ClockIcon } from "~/components/icons/Clock";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { UserIcon } from "~/components/icons/User";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { getUser, requireUser } from "~/features/auth/core/user.server";
|
||||
import { imgTypeToDimensions } from "~/features/img-upload/upload-constants";
|
||||
import { MapPool } from "~/features/map-list-generator/core/map-pool";
|
||||
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
|
||||
import { ModeMapPoolPicker } from "~/features/sendouq-settings/components/ModeMapPoolPicker";
|
||||
import * as QRepository from "~/features/sendouq/QRepository.server";
|
||||
import type { TournamentData } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import {
|
||||
clearTournamentDataCache,
|
||||
tournamentFromDB,
|
||||
type TournamentDataTeam,
|
||||
} from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { type TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { useAutoRerender } from "~/hooks/useAutoRerender";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { filterOutFalsy } from "~/utils/arrays";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix";
|
||||
import { booleanToInt } from "~/utils/sql";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import invariant from "~/utils/invariant";
|
||||
import {
|
||||
LOG_IN_URL,
|
||||
SENDOU_INK_BASE_URL,
|
||||
|
|
@ -54,244 +45,21 @@ import {
|
|||
tournamentSubsPage,
|
||||
userEditProfilePage,
|
||||
userPage,
|
||||
userSubmittedImage,
|
||||
} from "~/utils/urls";
|
||||
import { checkIn } from "../queries/checkIn.server";
|
||||
import { createTeam } from "../queries/createTeam.server";
|
||||
import { deleteTeam } from "../queries/deleteTeam.server";
|
||||
import deleteTeamMember from "../queries/deleteTeamMember.server";
|
||||
import { findByIdentifier } from "../queries/findByIdentifier.server";
|
||||
import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server";
|
||||
import { joinTeam } from "../queries/joinLeaveTeam.server";
|
||||
import { updateTeamInfo } from "../queries/updateTeamInfo.server";
|
||||
import { upsertCounterpickMaps } from "../queries/upsertCounterpickMaps.server";
|
||||
import { TOURNAMENT } from "../tournament-constants";
|
||||
import { registerSchema } from "../tournament-schemas.server";
|
||||
import {
|
||||
isOneModeTournamentOf,
|
||||
tournamentIdFromParams,
|
||||
type CounterPickValidationStatus,
|
||||
validateCounterPickMapPool,
|
||||
} from "../tournament-utils";
|
||||
import { useTournament } from "./to.$id";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
import { NewTabs } from "~/components/NewTabs";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { inGameNameIfNeeded } from "../tournament-utils.server";
|
||||
import type { TournamentRegisterPageLoader } from "../loaders/to.$id.register.server";
|
||||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const user = await requireUser(request);
|
||||
const data = await parseRequestFormData({ request, schema: registerSchema });
|
||||
import { loader } from "../loaders/to.$id.register.server";
|
||||
import { action } from "../actions/to.$id.register.server";
|
||||
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const tournament = await tournamentFromDB({ tournamentId, user });
|
||||
const event = notFoundIfFalsy(findByIdentifier(tournamentId));
|
||||
|
||||
validate(
|
||||
!tournament.hasStarted,
|
||||
"Tournament has started, cannot make edits to registration",
|
||||
);
|
||||
|
||||
const ownTeam = tournament.ownedTeamByUser(user);
|
||||
const ownTeamCheckedIn = Boolean(ownTeam && ownTeam.checkIns.length > 0);
|
||||
|
||||
switch (data._action) {
|
||||
case "UPSERT_TEAM": {
|
||||
validate(
|
||||
!data.teamId ||
|
||||
(await TeamRepository.findByUserId(user.id))?.id === data.teamId,
|
||||
"Team id does not match the team you are in",
|
||||
);
|
||||
|
||||
if (ownTeam) {
|
||||
validate(
|
||||
tournament.registrationOpen || data.teamName === ownTeam.name,
|
||||
"Can't change team name after registration has closed",
|
||||
);
|
||||
|
||||
updateTeamInfo({
|
||||
name: data.teamName,
|
||||
id: ownTeam.id,
|
||||
prefersNotToHost: booleanToInt(data.prefersNotToHost),
|
||||
noScreen: booleanToInt(data.noScreen),
|
||||
teamId: data.teamId ?? null,
|
||||
});
|
||||
} else {
|
||||
validate(!tournament.isInvitational, "Event is invite only");
|
||||
validate(
|
||||
(await UserRepository.findLeanById(user.id))?.friendCode,
|
||||
"No friend code",
|
||||
);
|
||||
validate(
|
||||
!tournament.teamMemberOfByUser(user),
|
||||
"You are already in a team that you aren't captain of",
|
||||
);
|
||||
validate(tournament.registrationOpen, "Registration is closed");
|
||||
|
||||
createTeam({
|
||||
name: data.teamName,
|
||||
tournamentId: tournamentId,
|
||||
ownerId: user.id,
|
||||
prefersNotToHost: booleanToInt(data.prefersNotToHost),
|
||||
noScreen: booleanToInt(data.noScreen),
|
||||
ownerInGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: user.id,
|
||||
}),
|
||||
teamId: data.teamId ?? null,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "DELETE_TEAM_MEMBER": {
|
||||
validate(ownTeam);
|
||||
validate(ownTeam.members.some((member) => member.userId === data.userId));
|
||||
validate(data.userId !== user.id);
|
||||
|
||||
const detailedOwnTeam = findOwnTournamentTeam({
|
||||
tournamentId,
|
||||
userId: user.id,
|
||||
});
|
||||
// making sure they aren't unfilling one checking in condition i.e. having full roster
|
||||
// and then having members kicked without it affecting the checking in status
|
||||
validate(
|
||||
detailedOwnTeam &&
|
||||
(!detailedOwnTeam.checkedInAt ||
|
||||
ownTeam.members.length > TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL),
|
||||
);
|
||||
|
||||
deleteTeamMember({ tournamentTeamId: ownTeam.id, userId: data.userId });
|
||||
break;
|
||||
}
|
||||
case "LEAVE_TEAM": {
|
||||
validate(!ownTeam, "Can't leave a team as the owner");
|
||||
|
||||
const teamMemberOf = tournament.teamMemberOfByUser(user);
|
||||
validate(teamMemberOf, "You are not in a team");
|
||||
validate(
|
||||
teamMemberOf.checkIns.length === 0,
|
||||
"You cannot leave after checking in",
|
||||
);
|
||||
|
||||
deleteTeamMember({
|
||||
tournamentTeamId: teamMemberOf.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "UPDATE_MAP_POOL": {
|
||||
const mapPool = new MapPool(data.mapPool);
|
||||
validate(ownTeam);
|
||||
validate(
|
||||
validateCounterPickMapPool(
|
||||
mapPool,
|
||||
isOneModeTournamentOf(event),
|
||||
tournament.ctx.tieBreakerMapPool,
|
||||
) === "VALID",
|
||||
);
|
||||
|
||||
upsertCounterpickMaps({
|
||||
tournamentTeamId: ownTeam.id,
|
||||
mapPool: new MapPool(data.mapPool),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "CHECK_IN": {
|
||||
logger.info(
|
||||
`Checking in (try): owned tournament team id: ${ownTeam?.id} - user id: ${user.id} - tournament id: ${tournamentId}`,
|
||||
);
|
||||
|
||||
const teamMemberOf = tournament.teamMemberOfByUser(user);
|
||||
validate(teamMemberOf, "You are not in a team");
|
||||
validate(
|
||||
teamMemberOf.checkIns.length === 0,
|
||||
"You have already checked in",
|
||||
);
|
||||
|
||||
validate(tournament.regularCheckInIsOpen, "Check in is not open");
|
||||
validate(
|
||||
tournament.checkInConditionsFulfilledByTeamId(teamMemberOf.id),
|
||||
"Check in conditions not fulfilled",
|
||||
);
|
||||
|
||||
checkIn(teamMemberOf.id);
|
||||
logger.info(
|
||||
`Checking in (success): tournament team id: ${teamMemberOf.id} - user id: ${user.id} - tournament id: ${tournamentId}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "ADD_PLAYER": {
|
||||
validate(
|
||||
tournament.ctx.teams.every((team) =>
|
||||
team.members.every((member) => member.userId !== data.userId),
|
||||
),
|
||||
"User is already in a team",
|
||||
);
|
||||
validate(ownTeam);
|
||||
validate(
|
||||
(await QRepository.usersThatTrusted(user.id)).some(
|
||||
(trusterPlayer) => trusterPlayer.id === data.userId,
|
||||
),
|
||||
"No trust given from this user",
|
||||
);
|
||||
validate(
|
||||
(await UserRepository.findLeanById(user.id))?.friendCode,
|
||||
"No friend code",
|
||||
);
|
||||
validate(tournament.registrationOpen, "Registration is closed");
|
||||
|
||||
joinTeam({
|
||||
userId: data.userId,
|
||||
newTeamId: ownTeam.id,
|
||||
tournamentId,
|
||||
inGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: data.userId,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "UNREGISTER": {
|
||||
validate(ownTeam, "You are not registered to this tournament");
|
||||
validate(!ownTeamCheckedIn, "You cannot unregister after checking in");
|
||||
|
||||
deleteTeam(ownTeam.id);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
}
|
||||
|
||||
clearTournamentDataCache(tournamentId);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type TournamentRegisterPageLoader = typeof loader;
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await getUser(request);
|
||||
if (!user) return null;
|
||||
|
||||
const ownTournamentTeam = findOwnTournamentTeam({
|
||||
tournamentId: tournamentIdFromParams(params),
|
||||
userId: user.id,
|
||||
});
|
||||
if (!ownTournamentTeam)
|
||||
return {
|
||||
mapPool: null,
|
||||
trusterPlayers: null,
|
||||
team: await TeamRepository.findByUserId(user.id),
|
||||
};
|
||||
|
||||
return {
|
||||
mapPool: findMapPoolByTeamId(ownTournamentTeam.id),
|
||||
trusterPlayers: await QRepository.usersThatTrusted(user.id),
|
||||
team: await TeamRepository.findByUserId(user.id),
|
||||
};
|
||||
};
|
||||
export { loader, action };
|
||||
|
||||
export default function TournamentRegisterPage() {
|
||||
const user = useUser();
|
||||
|
|
@ -527,7 +295,7 @@ function PleaseLogIn() {
|
|||
}
|
||||
|
||||
function RegistrationForms() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const data = useLoaderData<TournamentRegisterPageLoader>();
|
||||
const user = useUser();
|
||||
const tournament = useTournament();
|
||||
|
||||
|
|
@ -571,9 +339,7 @@ function RegistrationForms() {
|
|||
<FriendCode />
|
||||
{user?.friendCode ? (
|
||||
<TeamInfo
|
||||
name={ownTeam?.name}
|
||||
prefersNotToHost={ownTeam?.prefersNotToHost}
|
||||
noScreen={ownTeam?.noScreen}
|
||||
ownTeam={ownTeam}
|
||||
canUnregister={Boolean(ownTeam && !ownTeamCheckedIn)}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -784,25 +550,23 @@ function CheckIn({
|
|||
}
|
||||
|
||||
function TeamInfo({
|
||||
name,
|
||||
prefersNotToHost = 0,
|
||||
noScreen = 0,
|
||||
ownTeam,
|
||||
canUnregister,
|
||||
}: {
|
||||
name?: string;
|
||||
prefersNotToHost?: number;
|
||||
noScreen?: number;
|
||||
ownTeam?: TournamentDataTeam | null;
|
||||
canUnregister: boolean;
|
||||
}) {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const data = useLoaderData<TournamentRegisterPageLoader>();
|
||||
const { t } = useTranslation(["tournament", "common"]);
|
||||
const fetcher = useFetcher();
|
||||
const tournament = useTournament();
|
||||
const [teamName, setTeamName] = React.useState(name);
|
||||
const [teamName, setTeamName] = React.useState(ownTeam?.name ?? "");
|
||||
const user = useUser();
|
||||
const ref = React.useRef<HTMLFormElement>(null);
|
||||
const [signUpWithTeam, setSignUpWithTeam] = React.useState(() =>
|
||||
Boolean(tournament.ownedTeamByUser(user)?.team),
|
||||
);
|
||||
const [uploadedAvatar, setUploadedAvatar] = React.useState<File | null>(null);
|
||||
|
||||
const handleSignUpWithTeamChange = (checked: boolean) => {
|
||||
if (!checked) {
|
||||
|
|
@ -813,6 +577,50 @@ function TeamInfo({
|
|||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const formData = new FormData(ref.current!);
|
||||
|
||||
if (uploadedAvatar) {
|
||||
// replace with the compressed version
|
||||
formData.delete("img");
|
||||
formData.append("img", uploadedAvatar, uploadedAvatar.name);
|
||||
}
|
||||
|
||||
fetcher.submit(formData, {
|
||||
encType: uploadedAvatar ? "multipart/form-data" : undefined,
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
const submitButtonDisabled = () => {
|
||||
if (fetcher.state !== "idle") return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const avatarUrl = (() => {
|
||||
if (signUpWithTeam) {
|
||||
if (ownTeam?.team?.logoUrl) {
|
||||
return userSubmittedImage(ownTeam.team.logoUrl);
|
||||
}
|
||||
return data?.team?.logoUrl ? userSubmittedImage(data.team.logoUrl) : null;
|
||||
}
|
||||
if (uploadedAvatar) return URL.createObjectURL(uploadedAvatar);
|
||||
if (ownTeam?.pickupAvatarUrl) {
|
||||
return userSubmittedImage(ownTeam.pickupAvatarUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
const canEditAvatar =
|
||||
tournament.registrationOpen &&
|
||||
!signUpWithTeam &&
|
||||
uploadedAvatar &&
|
||||
!ownTeam?.pickupAvatarUrl;
|
||||
|
||||
const canDeleteAvatar = ownTeam?.pickupAvatarUrl;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="stack horizontal justify-between">
|
||||
|
|
@ -829,7 +637,6 @@ function TeamInfo({
|
|||
className="build__small-text"
|
||||
variant="minimal-destructive"
|
||||
size="tiny"
|
||||
type="submit"
|
||||
>
|
||||
{t("tournament:pre.info.unregister")}
|
||||
</Button>
|
||||
|
|
@ -837,7 +644,8 @@ function TeamInfo({
|
|||
) : null}
|
||||
</div>
|
||||
<section className="tournament__section">
|
||||
<fetcher.Form method="post" className="stack md items-center">
|
||||
<Form method="post" className="stack md items-center" ref={ref}>
|
||||
<input type="hidden" name="_action" value="UPSERT_TEAM" />
|
||||
{signUpWithTeam && data?.team ? (
|
||||
<input type="hidden" name="teamId" value={data.team.id} />
|
||||
) : null}
|
||||
|
|
@ -867,13 +675,48 @@ function TeamInfo({
|
|||
readOnly={!tournament.registrationOpen || signUpWithTeam}
|
||||
/>
|
||||
</div>
|
||||
{tournament.registrationOpen || avatarUrl ? (
|
||||
<div className="tournament__section__input-container">
|
||||
<Label htmlFor="logo">Logo</Label>
|
||||
{avatarUrl ? (
|
||||
<div className="stack horizontal md items-center">
|
||||
<Avatar size="xsm" url={avatarUrl} />
|
||||
{canEditAvatar ? (
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="tiny"
|
||||
onClick={() => setUploadedAvatar(null)}
|
||||
>
|
||||
{t("common:actions.edit")}
|
||||
</Button>
|
||||
) : null}
|
||||
{canDeleteAvatar ? (
|
||||
<FormWithConfirm
|
||||
dialogHeading="Delete team logo?"
|
||||
fields={[["_action", "DELETE_LOGO"]]}
|
||||
>
|
||||
<Button
|
||||
variant="minimal-destructive"
|
||||
size="tiny"
|
||||
type="submit"
|
||||
>
|
||||
<TrashIcon className="small-icon" />
|
||||
</Button>
|
||||
</FormWithConfirm>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<TournamentLogoUpload onChange={setUploadedAvatar} />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="stack sm">
|
||||
<div className="text-lighter text-sm stack horizontal sm items-center">
|
||||
<input
|
||||
id="no-host"
|
||||
type="checkbox"
|
||||
name="prefersNotToHost"
|
||||
defaultChecked={Boolean(prefersNotToHost)}
|
||||
defaultChecked={Boolean(ownTeam?.prefersNotToHost)}
|
||||
/>
|
||||
<label htmlFor="no-host" className="mb-0">
|
||||
{t("tournament:pre.info.noHost")}
|
||||
|
|
@ -886,7 +729,7 @@ function TeamInfo({
|
|||
id="no-screen"
|
||||
type="checkbox"
|
||||
name="noScreen"
|
||||
defaultChecked={Boolean(noScreen)}
|
||||
defaultChecked={Boolean(ownTeam?.noScreen)}
|
||||
data-testid="no-screen-checkbox"
|
||||
/>
|
||||
<label htmlFor="no-screen" className="mb-0">
|
||||
|
|
@ -896,19 +739,62 @@ function TeamInfo({
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<SubmitButton
|
||||
_action="UPSERT_TEAM"
|
||||
state={fetcher.state}
|
||||
<Button
|
||||
testId="save-team-button"
|
||||
disabled={submitButtonDisabled()}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t("common:actions.save")}
|
||||
</SubmitButton>
|
||||
</fetcher.Form>
|
||||
</Button>
|
||||
</Form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const logoDimensions = imgTypeToDimensions["team-pfp"];
|
||||
function TournamentLogoUpload({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (file: File | null) => void;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
id="img-field"
|
||||
className="plain"
|
||||
type="file"
|
||||
name="img"
|
||||
accept="image/png, image/jpeg, image/webp"
|
||||
onChange={(e) => {
|
||||
const uploadedFile = e.target.files?.[0];
|
||||
if (!uploadedFile) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
new Compressor(uploadedFile, {
|
||||
height: logoDimensions.height,
|
||||
width: logoDimensions.width,
|
||||
maxHeight: logoDimensions.height,
|
||||
maxWidth: logoDimensions.width,
|
||||
// 0.5MB
|
||||
convertSize: 500_000,
|
||||
resize: "cover",
|
||||
success(result) {
|
||||
const file = new File([result], `img.webp`, {
|
||||
type: "image/webp",
|
||||
});
|
||||
onChange(file);
|
||||
},
|
||||
error(err) {
|
||||
console.error(err.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FriendCode() {
|
||||
const user = useUser();
|
||||
|
||||
|
|
@ -936,7 +822,7 @@ function FillRoster({
|
|||
ownTeam: TournamentDataTeam;
|
||||
ownTeamCheckedIn: boolean;
|
||||
}) {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const data = useLoaderData<TournamentRegisterPageLoader>();
|
||||
const user = useUser();
|
||||
const tournament = useTournament();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
|
@ -1159,7 +1045,7 @@ function CounterPickMapPoolPicker() {
|
|||
const { t } = useTranslation(["common", "game-misc", "tournament"]);
|
||||
const tournament = useTournament();
|
||||
const fetcher = useFetcher();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const data = useLoaderData<TournamentRegisterPageLoader>();
|
||||
const [counterPickMaps, setCounterPickMaps] = React.useState(
|
||||
data?.mapPool ?? [],
|
||||
);
|
||||
|
|
@ -1269,81 +1155,6 @@ function MapPoolValidationStatusMessage({
|
|||
);
|
||||
}
|
||||
|
||||
type CounterPickValidationStatus =
|
||||
| "PICKING"
|
||||
| "VALID"
|
||||
| "TOO_MUCH_STAGE_REPEAT"
|
||||
| "STAGE_REPEAT_IN_SAME_MODE"
|
||||
| "INCLUDES_BANNED"
|
||||
| "INCLUDES_TIEBREAKER";
|
||||
|
||||
function validateCounterPickMapPool(
|
||||
mapPool: MapPool,
|
||||
isOneModeOnlyTournamentFor: ModeShort | null,
|
||||
tieBreakerMapPool: TournamentData["ctx"]["tieBreakerMapPool"],
|
||||
): CounterPickValidationStatus {
|
||||
const stageCounts = new Map<StageId, number>();
|
||||
for (const stageId of mapPool.stages) {
|
||||
if (!stageCounts.has(stageId)) {
|
||||
stageCounts.set(stageId, 0);
|
||||
}
|
||||
|
||||
if (
|
||||
stageCounts.get(stageId)! >= TOURNAMENT.COUNTERPICK_MAX_STAGE_REPEAT ||
|
||||
(isOneModeOnlyTournamentFor && stageCounts.get(stageId)! >= 1)
|
||||
) {
|
||||
return "TOO_MUCH_STAGE_REPEAT";
|
||||
}
|
||||
|
||||
stageCounts.set(stageId, stageCounts.get(stageId)! + 1);
|
||||
}
|
||||
|
||||
if (
|
||||
new MapPool(mapPool.serialized).stageModePairs.length !==
|
||||
mapPool.stageModePairs.length
|
||||
) {
|
||||
return "STAGE_REPEAT_IN_SAME_MODE";
|
||||
}
|
||||
|
||||
if (
|
||||
mapPool.stageModePairs.some((pair) =>
|
||||
BANNED_MAPS[pair.mode].includes(pair.stageId),
|
||||
)
|
||||
) {
|
||||
return "INCLUDES_BANNED";
|
||||
}
|
||||
|
||||
if (
|
||||
mapPool.stageModePairs.some((pair) =>
|
||||
tieBreakerMapPool.some(
|
||||
(stage) => stage.mode === pair.mode && stage.stageId === pair.stageId,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return "INCLUDES_TIEBREAKER";
|
||||
}
|
||||
|
||||
if (
|
||||
!isOneModeOnlyTournamentFor &&
|
||||
(mapPool.parsed.SZ.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
|
||||
mapPool.parsed.TC.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
|
||||
mapPool.parsed.RM.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
|
||||
mapPool.parsed.CB.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE)
|
||||
) {
|
||||
return "PICKING";
|
||||
}
|
||||
|
||||
if (
|
||||
isOneModeOnlyTournamentFor &&
|
||||
mapPool.parsed[isOneModeOnlyTournamentFor].length !==
|
||||
TOURNAMENT.COUNTERPICK_ONE_MODE_TOURNAMENT_MAPS_PER_MODE
|
||||
) {
|
||||
return "PICKING";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
function TOPickedMapPoolInfo() {
|
||||
const { t } = useTranslation(["calendar"]);
|
||||
const tournament = useTournament();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ export const registerSchema = z.union([
|
|||
z.object({
|
||||
_action: _action("UNREGISTER"),
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("DELETE_LOGO"),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const seedsActionSchema = z.object({
|
||||
|
|
@ -109,6 +112,10 @@ export const adminActionSchema = z.union([
|
|||
_action: _action("UNDO_DROP_TEAM_OUT"),
|
||||
teamId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("DELETE_LOGO"),
|
||||
teamId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("UPDATE_CAST_TWITCH_ACCOUNTS"),
|
||||
castTwitchAccounts: z.preprocess(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import type { Params } from "@remix-run/react";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { Tournament } from "~/db/types";
|
||||
import type { ModeShort } from "~/modules/in-game-lists";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { tournamentLogoUrl } from "~/utils/urls";
|
||||
import type { PlayedSet } from "./core/sets.server";
|
||||
import { MapPool } from "../map-list-generator/core/map-pool";
|
||||
import type { TournamentData } from "../tournament-bracket/core/Tournament.server";
|
||||
import { TOURNAMENT } from "./tournament-constants";
|
||||
import { BANNED_MAPS } from "../sendouq-settings/banned-maps";
|
||||
|
||||
export function tournamentIdFromParams(params: Params<string>) {
|
||||
const result = Number(params["id"]);
|
||||
|
|
@ -307,3 +311,78 @@ export function HACKY_resolveThemeColors(event: { name: string }) {
|
|||
|
||||
return { backgroundColor: "#3430ad", textColor: WHITE };
|
||||
}
|
||||
|
||||
export type CounterPickValidationStatus =
|
||||
| "PICKING"
|
||||
| "VALID"
|
||||
| "TOO_MUCH_STAGE_REPEAT"
|
||||
| "STAGE_REPEAT_IN_SAME_MODE"
|
||||
| "INCLUDES_BANNED"
|
||||
| "INCLUDES_TIEBREAKER";
|
||||
|
||||
export function validateCounterPickMapPool(
|
||||
mapPool: MapPool,
|
||||
isOneModeOnlyTournamentFor: ModeShort | null,
|
||||
tieBreakerMapPool: TournamentData["ctx"]["tieBreakerMapPool"],
|
||||
): CounterPickValidationStatus {
|
||||
const stageCounts = new Map<StageId, number>();
|
||||
for (const stageId of mapPool.stages) {
|
||||
if (!stageCounts.has(stageId)) {
|
||||
stageCounts.set(stageId, 0);
|
||||
}
|
||||
|
||||
if (
|
||||
stageCounts.get(stageId)! >= TOURNAMENT.COUNTERPICK_MAX_STAGE_REPEAT ||
|
||||
(isOneModeOnlyTournamentFor && stageCounts.get(stageId)! >= 1)
|
||||
) {
|
||||
return "TOO_MUCH_STAGE_REPEAT";
|
||||
}
|
||||
|
||||
stageCounts.set(stageId, stageCounts.get(stageId)! + 1);
|
||||
}
|
||||
|
||||
if (
|
||||
new MapPool(mapPool.serialized).stageModePairs.length !==
|
||||
mapPool.stageModePairs.length
|
||||
) {
|
||||
return "STAGE_REPEAT_IN_SAME_MODE";
|
||||
}
|
||||
|
||||
if (
|
||||
mapPool.stageModePairs.some((pair) =>
|
||||
BANNED_MAPS[pair.mode].includes(pair.stageId),
|
||||
)
|
||||
) {
|
||||
return "INCLUDES_BANNED";
|
||||
}
|
||||
|
||||
if (
|
||||
mapPool.stageModePairs.some((pair) =>
|
||||
tieBreakerMapPool.some(
|
||||
(stage) => stage.mode === pair.mode && stage.stageId === pair.stageId,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return "INCLUDES_TIEBREAKER";
|
||||
}
|
||||
|
||||
if (
|
||||
!isOneModeOnlyTournamentFor &&
|
||||
(mapPool.parsed.SZ.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
|
||||
mapPool.parsed.TC.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
|
||||
mapPool.parsed.RM.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
|
||||
mapPool.parsed.CB.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE)
|
||||
) {
|
||||
return "PICKING";
|
||||
}
|
||||
|
||||
if (
|
||||
isOneModeOnlyTournamentFor &&
|
||||
mapPool.parsed[isOneModeOnlyTournamentFor].length !==
|
||||
TOURNAMENT.COUNTERPICK_ONE_MODE_TOURNAMENT_MAPS_PER_MODE
|
||||
) {
|
||||
return "PICKING";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -952,6 +952,11 @@ dialog::backdrop {
|
|||
height: unset;
|
||||
}
|
||||
|
||||
.small-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
|
||||
/* https://stackoverflow.com/questions/50917016/make-a-hidden-field-required/50917245#comment117565184_50917245 */
|
||||
.hidden-input-with-validation {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@ import type navItems from "~/components/layout/nav-items.json";
|
|||
import { json } from "@remix-run/node";
|
||||
import type { Namespace, TFunction } from "i18next";
|
||||
import { noticeError } from "./newrelic.server";
|
||||
import {
|
||||
unstable_composeUploadHandlers as composeUploadHandlers,
|
||||
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
|
||||
unstable_parseMultipartFormData as parseMultipartFormData,
|
||||
} from "@remix-run/node";
|
||||
import { s3UploadHandler } from "~/features/img-upload";
|
||||
import { nanoid } from "nanoid";
|
||||
import invariant from "./invariant";
|
||||
|
||||
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
|
||||
if (!value) throw new Response(null, { status: 404 });
|
||||
|
|
@ -243,3 +251,41 @@ export function privatelyCachedJson<T>(data: T) {
|
|||
headers: { "Cache-Control": "private, max-age=5" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadImageIfSubmitted({
|
||||
request,
|
||||
fileNamePrefix,
|
||||
}: {
|
||||
request: Request;
|
||||
fileNamePrefix: string;
|
||||
}) {
|
||||
const uploadHandler = composeUploadHandlers(
|
||||
s3UploadHandler(`${fileNamePrefix}-${nanoid()}-${Date.now()}`),
|
||||
createMemoryUploadHandler(),
|
||||
);
|
||||
|
||||
try {
|
||||
const formData = await parseMultipartFormData(request, uploadHandler);
|
||||
const imgSrc = formData.get("img") as string | null;
|
||||
invariant(imgSrc);
|
||||
|
||||
const urlParts = imgSrc.split("/");
|
||||
const fileName = urlParts[urlParts.length - 1];
|
||||
invariant(fileName);
|
||||
|
||||
return {
|
||||
avatarFileName: fileName,
|
||||
formData,
|
||||
};
|
||||
} catch (err) {
|
||||
// user did not submit image
|
||||
if (err instanceof TypeError) {
|
||||
return {
|
||||
avatarFileName: undefined,
|
||||
formData: await request.formData(),
|
||||
};
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
migrations/062-tournament-team-avatar.js
Normal file
7
migrations/062-tournament-team-avatar.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/* sql */ `alter table "TournamentTeam" add "avatarImgId" integer`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user