mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-23 19:46:28 -05:00
Subs registration closing when regular registration closes feature
This commit is contained in:
@@ -12,7 +12,10 @@ import {
|
||||
} from "~/features/tournament/tournament-utils";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import type { ModeShort } from "~/modules/in-game-lists";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import {
|
||||
databaseTimestampToDate,
|
||||
dateToDatabaseTimestamp,
|
||||
} from "~/utils/dates";
|
||||
import { fillWithNullTillPowerOfTwo } from "../tournament-bracket-utils";
|
||||
import type { Stage } from "~/modules/brackets-model";
|
||||
import { Bracket } from "./Bracket";
|
||||
@@ -636,6 +639,17 @@ export class Tournament {
|
||||
return !this.isInvitational();
|
||||
}
|
||||
|
||||
get canAddNewSubPost() {
|
||||
if (!this.subsFeatureEnabled) return false;
|
||||
|
||||
return (
|
||||
!this.ctx.settings.regClosesAt ||
|
||||
this.ctx.settings.regClosesAt ===
|
||||
dateToDatabaseTimestamp(this.ctx.startTime) ||
|
||||
this.registrationOpen
|
||||
);
|
||||
}
|
||||
|
||||
get maxTeamMemberCount() {
|
||||
const maxMembersBeforeStart = this.isInvitational()
|
||||
? 5
|
||||
|
||||
@@ -17,12 +17,17 @@ import { Button } from "~/components/Button";
|
||||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { findSubsByTournamentId } from "../queries/findSubsByTournamentId.server";
|
||||
import { tournamentIdFromParams } from "~/features/tournament";
|
||||
import { parseRequestFormData, type SendouRouteHandle } from "~/utils/remix";
|
||||
import {
|
||||
parseRequestFormData,
|
||||
validate,
|
||||
type SendouRouteHandle,
|
||||
} from "~/utils/remix";
|
||||
import { FormMessage } from "~/components/FormMessage";
|
||||
import { subSchema } from "../tournament-subs-schemas.server";
|
||||
import { upsertSub } from "../queries/upsertSub.server";
|
||||
import { tournamentSubsPage } from "~/utils/urls";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
|
||||
import "../tournament-subs.css";
|
||||
|
||||
@@ -37,8 +42,13 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
schema: subSchema,
|
||||
});
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const tournament = await tournamentFromDB({ tournamentId, user });
|
||||
|
||||
// TODO: validate tournament is not finalized
|
||||
validate(!tournament.everyBracketOver, "Tournament is over");
|
||||
validate(
|
||||
tournament.canAddNewSubPost,
|
||||
"Registration is closed or subs feature disabled",
|
||||
);
|
||||
|
||||
upsertSub({
|
||||
bestWeapons: data.bestWeapons.join(","),
|
||||
@@ -56,6 +66,11 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const user = await requireUser(request);
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const tournament = await tournamentFromDB({ tournamentId, user });
|
||||
|
||||
if (!tournament.canAddNewSubPost) {
|
||||
throw redirect(tournamentSubsPage(tournamentId));
|
||||
}
|
||||
|
||||
const sub = findSubsByTournamentId({ tournamentId }).find(
|
||||
(sub) => sub.userId === user.id,
|
||||
|
||||
@@ -27,16 +27,30 @@ import {
|
||||
findSubsByTournamentId,
|
||||
type SubByTournamentId,
|
||||
} from "../queries/findSubsByTournamentId.server";
|
||||
import { parseRequestFormData, validate } from "~/utils/remix";
|
||||
import { deleteSubSchema } from "../tournament-subs-schemas.server";
|
||||
import { Popover } from "~/components/Popover";
|
||||
|
||||
import "../tournament-subs.css";
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const user = await requireUser(request);
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const tournament = await tournamentFromDB({ tournamentId, user });
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
schema: deleteSubSchema,
|
||||
});
|
||||
|
||||
validate(
|
||||
user.id === data.userId || tournament.isOrganizer(user),
|
||||
"You can only delete your own sub post",
|
||||
401,
|
||||
);
|
||||
|
||||
deleteSub({
|
||||
tournamentId,
|
||||
userId: user.id,
|
||||
userId: data.userId,
|
||||
});
|
||||
|
||||
return null;
|
||||
@@ -84,7 +98,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
|
||||
export default function TournamentSubsPage() {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const tournament = useTournament();
|
||||
|
||||
@@ -96,11 +109,7 @@ export default function TournamentSubsPage() {
|
||||
<div className="stack lg">
|
||||
{!tournament.teamMemberOfByUser(user) && user ? (
|
||||
<div className="stack items-end">
|
||||
<LinkButton to="new" size="tiny">
|
||||
{data.hasOwnSubPost
|
||||
? t("tournament:subs.editPost")
|
||||
: t("tournament:subs.addPost")}
|
||||
</LinkButton>
|
||||
<AddOrEditSubButton />
|
||||
</div>
|
||||
) : null}
|
||||
{data.subs.map((sub) => {
|
||||
@@ -110,9 +119,36 @@ export default function TournamentSubsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function AddOrEditSubButton() {
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const tournament = useTournament();
|
||||
|
||||
const buttonText = data.hasOwnSubPost
|
||||
? t("tournament:subs.editPost")
|
||||
: t("tournament:subs.addPost");
|
||||
|
||||
if (!tournament.canAddNewSubPost) {
|
||||
return (
|
||||
<Popover buttonChildren={<>{buttonText}</>} triggerClassName="tiny">
|
||||
{data.hasOwnSubPost
|
||||
? "Sub post can't be edited anymore since registration has closed"
|
||||
: "Sub post can't be added anymore since registration has closed"}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkButton to="new" size="tiny">
|
||||
{buttonText}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SubInfoSection({ sub }: { sub: SubByTournamentId }) {
|
||||
const { t } = useTranslation(["common", "tournament"]);
|
||||
const user = useUser();
|
||||
const tournament = useTournament();
|
||||
|
||||
const infos = [
|
||||
<div key="vc" className="sub__section__info__vc">
|
||||
@@ -172,9 +208,16 @@ function SubInfoSection({ sub }: { sub: SubByTournamentId }) {
|
||||
<div className="sub__section__message">{sub.message}</div>
|
||||
) : null}
|
||||
</section>
|
||||
{user?.id === sub.userId ? (
|
||||
{user?.id === sub.userId || tournament.isOrganizer(user) ? (
|
||||
<div className="stack mt-1 items-end">
|
||||
<FormWithConfirm dialogHeading="Delete your sub post?">
|
||||
<FormWithConfirm
|
||||
dialogHeading={
|
||||
user?.id === sub.userId
|
||||
? "Delete your sub post?"
|
||||
: `Delete sub post by ${sub.discordName}?`
|
||||
}
|
||||
fields={[["userId", sub.userId]]}
|
||||
>
|
||||
<Button
|
||||
variant="minimal-destructive"
|
||||
size="tiny"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import {
|
||||
checkboxValueToDbBoolean,
|
||||
dbBoolean,
|
||||
id,
|
||||
processMany,
|
||||
removeDuplicates,
|
||||
safeJSONParse,
|
||||
@@ -39,3 +40,7 @@ export const subSchema = z.object({
|
||||
message: z.string().max(TOURNAMENT_SUB.MESSAGE_MAX_LENGTH).nullish(),
|
||||
visibility: z.enum(["+1", "+2", "+3", "ALL"]).default("ALL"),
|
||||
});
|
||||
|
||||
export const deleteSubSchema = z.object({
|
||||
userId: id,
|
||||
});
|
||||
|
||||
@@ -356,7 +356,7 @@ export default function TournamentRegisterPage() {
|
||||
) : (
|
||||
<RegistrationForms />
|
||||
)}
|
||||
{!tournament.teamMemberOfByUser(user) && tournament.subsFeatureEnabled ? (
|
||||
{!tournament.teamMemberOfByUser(user) && tournament.canAddNewSubPost ? (
|
||||
<Link
|
||||
to={tournamentSubsPage(tournament.ctx.id)}
|
||||
className="text-xs text-center"
|
||||
|
||||
Reference in New Issue
Block a user