From 40ac14eb688101ceceda72b5ca4495a8e34f6b1a Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:20:55 +0300 Subject: [PATCH] Auto-cancel overlapping pending scrim posts/requests on booking Closes #3150 --- app/components/FormWithConfirm.tsx | 7 + .../notifications/core/notify.server.ts | 1 + .../notifications/notifications-types.ts | 1 + .../notifications/notifications-utils.ts | 4 +- .../scrims/ScrimPostRepository.server.test.ts | 206 ++++++++++++++++++ .../scrims/ScrimPostRepository.server.ts | 98 +++++++++ app/features/scrims/actions/scrims.server.ts | 44 +++- app/features/scrims/components/ScrimCard.tsx | 1 + .../scrims/components/ScrimRequestModal.tsx | 2 + app/features/scrims/scrims-constants.ts | 1 + e2e/scrims.spec.ts | 53 +++++ locales/da/common.json | 2 + locales/da/scrims.json | 1 + locales/de/common.json | 2 + locales/de/scrims.json | 1 + locales/en/common.json | 2 + locales/en/scrims.json | 1 + locales/es-ES/common.json | 2 + locales/es-ES/scrims.json | 1 + locales/es-US/common.json | 2 + locales/es-US/scrims.json | 1 + locales/fr-CA/common.json | 2 + locales/fr-CA/scrims.json | 1 + locales/fr-EU/common.json | 2 + locales/fr-EU/scrims.json | 1 + locales/he/common.json | 2 + locales/he/scrims.json | 1 + locales/it/common.json | 2 + locales/it/scrims.json | 1 + locales/ja/common.json | 2 + locales/ja/scrims.json | 1 + locales/ko/common.json | 2 + locales/ko/scrims.json | 1 + locales/nl/common.json | 2 + locales/nl/scrims.json | 1 + locales/pl/common.json | 2 + locales/pl/scrims.json | 1 + locales/pt-BR/common.json | 2 + locales/pt-BR/scrims.json | 1 + locales/ru/common.json | 2 + locales/ru/scrims.json | 1 + locales/zh/common.json | 2 + locales/zh/scrims.json | 1 + 43 files changed, 464 insertions(+), 2 deletions(-) create mode 100644 app/features/scrims/ScrimPostRepository.server.test.ts diff --git a/app/components/FormWithConfirm.tsx b/app/components/FormWithConfirm.tsx index 9ddaece0d..7709d2723 100644 --- a/app/components/FormWithConfirm.tsx +++ b/app/components/FormWithConfirm.tsx @@ -6,6 +6,7 @@ import type { SendouButtonProps } from "~/components/elements/Button"; import { SendouDialog } from "~/components/elements/Dialog"; import { useHydrated } from "~/hooks/useHydrated"; import invariant from "~/utils/invariant"; +import { FormMessage } from "./FormMessage"; import { SubmitButton } from "./SubmitButton"; interface ChildProps { @@ -17,6 +18,7 @@ export function FormWithConfirm({ fields, children, dialogHeading, + description, submitButtonText, action, submitButtonTestId = "submit-button", @@ -31,6 +33,8 @@ export function FormWithConfirm({ )[]; children?: React.ReactElement; dialogHeading: string; + /** Optional explanatory text shown below the heading in the confirm dialog */ + description?: React.ReactNode; submitButtonText?: string; action?: string; submitButtonTestId?: string; @@ -96,6 +100,9 @@ export function FormWithConfirm({ >

{dialogHeading}

+ {description ? ( + {description} + ) : null}
= { SCRIM_SCHEDULED: "high", SCRIM_CANCELED: "high", SCRIM_STARTING_SOON: "high", + SCRIM_AUTO_DELETED: "normal", COMMISSIONS_CLOSED: "normal", FRIEND_REQUEST_RECEIVED: "normal", }; diff --git a/app/features/notifications/notifications-types.ts b/app/features/notifications/notifications-types.ts index f213d2a1e..7c47aa34f 100644 --- a/app/features/notifications/notifications-types.ts +++ b/app/features/notifications/notifications-types.ts @@ -71,6 +71,7 @@ export type Notification = "SCRIM_STARTING_SOON", { id: number; opponentTeamName: string } > + | NotificationItem<"SCRIM_AUTO_DELETED", { at: number }> | NotificationItem<"COMMISSIONS_CLOSED", { discordId: string }> | NotificationItem<"FRIEND_REQUEST_RECEIVED", { senderUsername: string }> | NotificationItem< diff --git a/app/features/notifications/notifications-utils.ts b/app/features/notifications/notifications-utils.ts index 642cb87a6..d2336245b 100644 --- a/app/features/notifications/notifications-utils.ts +++ b/app/features/notifications/notifications-utils.ts @@ -43,6 +43,7 @@ export const notificationNavIcon = (type: Notification["type"]) => { case "SCRIM_SCHEDULED": case "SCRIM_CANCELED": case "SCRIM_STARTING_SOON": + case "SCRIM_AUTO_DELETED": return "scrims"; case "FRIEND_REQUEST_RECEIVED": return "sendou_love"; @@ -85,7 +86,8 @@ export const notificationLink = (notification: Notification) => { case "TO_TEST_CREATED": case "TO_CHECK_IN_OPENED": return tournamentRegisterPage(notification.meta.tournamentId); - case "SCRIM_NEW_REQUEST": { + case "SCRIM_NEW_REQUEST": + case "SCRIM_AUTO_DELETED": { return scrimsPage(); } case "SCRIM_CANCELED": diff --git a/app/features/scrims/ScrimPostRepository.server.test.ts b/app/features/scrims/ScrimPostRepository.server.test.ts new file mode 100644 index 000000000..893a23331 --- /dev/null +++ b/app/features/scrims/ScrimPostRepository.server.test.ts @@ -0,0 +1,206 @@ +import { add, sub } from "date-fns"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; +import { dbInsertUsers, dbReset } from "~/utils/Test"; +import * as ScrimPostRepository from "./ScrimPostRepository.server"; + +const BOOKED_AT = add(new Date(), { hours: 10 }); + +const dbTs = (date: Date) => dateToDatabaseTimestamp(date); + +const WINDOW = { + startTime: dbTs(sub(BOOKED_AT, { hours: 1 })), + endTime: dbTs(add(BOOKED_AT, { hours: 1 })), +}; + +function insertPost({ + at, + rangeEnd = null, + users, +}: { + at: Date; + rangeEnd?: Date | null; + users: Array<{ userId: number; isOwner: 0 | 1 }>; +}) { + return ScrimPostRepository.insert({ + at: dbTs(at), + rangeEnd: rangeEnd ? dbTs(rangeEnd) : null, + maxDiv: null, + minDiv: null, + teamId: null, + text: null, + maps: null, + mapsTournamentId: null, + users, + visibility: null, + managedByAnyone: false, + isScheduledForFuture: false, + }); +} + +describe("findPendingOverlapsForUsers", () => { + beforeEach(async () => { + await dbInsertUsers(5); + }); + + afterEach(() => { + dbReset(); + }); + + test("returns a specific-time pending post in window with its member ids", async () => { + const postId = await insertPost({ + at: BOOKED_AT, + users: [ + { userId: 1, isOwner: 1 }, + { userId: 2, isOwner: 0 }, + ], + }); + + const { posts } = await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1], + ...WINDOW, + excludePostId: -1, + }); + + expect(posts).toHaveLength(1); + expect(posts[0]!.id).toBe(postId); + expect(posts[0]!.memberIds.sort()).toEqual([1, 2]); + }); + + test("returns a ranged post whose interval overlaps the window even if its start is outside", async () => { + const postId = await insertPost({ + at: sub(BOOKED_AT, { hours: 2 }), + rangeEnd: BOOKED_AT, + users: [{ userId: 1, isOwner: 1 }], + }); + + const { posts } = await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1], + ...WINDOW, + excludePostId: -1, + }); + + expect(posts.map((p) => p.id)).toEqual([postId]); + }); + + test("does not return a ranged post whose interval does not overlap the window", async () => { + await insertPost({ + at: sub(BOOKED_AT, { hours: 5 }), + rangeEnd: sub(BOOKED_AT, { hours: 3 }), + users: [{ userId: 1, isOwner: 1 }], + }); + + const { posts } = await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1], + ...WINDOW, + excludePostId: -1, + }); + + expect(posts).toHaveLength(0); + }); + + test("excludes the just-booked post even when it overlaps", async () => { + const postId = await insertPost({ + at: BOOKED_AT, + users: [{ userId: 1, isOwner: 1 }], + }); + + const { posts } = await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1], + ...WINDOW, + excludePostId: postId, + }); + + expect(posts).toHaveLength(0); + }); + + test("does not return posts that involve none of the given users", async () => { + await insertPost({ + at: BOOKED_AT, + users: [{ userId: 3, isOwner: 1 }], + }); + + const { posts } = await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1, 2], + ...WINDOW, + excludePostId: -1, + }); + + expect(posts).toHaveLength(0); + }); + + test("excludes already-accepted (booked) posts", async () => { + const postId = await insertPost({ + at: BOOKED_AT, + users: [{ userId: 1, isOwner: 1 }], + }); + await ScrimPostRepository.insertRequest({ + scrimPostId: postId, + teamId: null, + message: null, + at: dbTs(BOOKED_AT), + users: [{ userId: 3, isOwner: 1 }], + }); + const post = await ScrimPostRepository.findById(postId); + await ScrimPostRepository.acceptRequest(post!.requests[0]!.id); + + const { posts, requestIds } = + await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1, 3], + ...WINDOW, + excludePostId: -1, + }); + + expect(posts).toHaveLength(0); + expect(requestIds).toHaveLength(0); + }); + + test("returns pending request ids whose effective time falls in the window", async () => { + const postId = await insertPost({ + at: add(BOOKED_AT, { hours: 3 }), + users: [{ userId: 3, isOwner: 1 }], + }); + await ScrimPostRepository.insertRequest({ + scrimPostId: postId, + teamId: null, + message: null, + at: dbTs(BOOKED_AT), + users: [{ userId: 1, isOwner: 1 }], + }); + const post = await ScrimPostRepository.findById(postId); + const requestId = post!.requests[0]!.id; + + const { posts, requestIds } = + await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1], + ...WINDOW, + excludePostId: -1, + }); + + expect(posts).toHaveLength(0); + expect(requestIds).toEqual([requestId]); + }); + + test("does not return pending requests whose effective time is outside the window", async () => { + const postId = await insertPost({ + at: add(BOOKED_AT, { hours: 3 }), + users: [{ userId: 3, isOwner: 1 }], + }); + await ScrimPostRepository.insertRequest({ + scrimPostId: postId, + teamId: null, + message: null, + at: dbTs(add(BOOKED_AT, { hours: 3 })), + users: [{ userId: 1, isOwner: 1 }], + }); + + const { requestIds } = + await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: [1], + ...WINDOW, + excludePostId: -1, + }); + + expect(requestIds).toHaveLength(0); + }); +}); diff --git a/app/features/scrims/ScrimPostRepository.server.ts b/app/features/scrims/ScrimPostRepository.server.ts index 550391dc3..7cc8881eb 100644 --- a/app/features/scrims/ScrimPostRepository.server.ts +++ b/app/features/scrims/ScrimPostRepository.server.ts @@ -437,6 +437,104 @@ export async function findAcceptedScrimsBetweenTwoTimestamps({ return rows.map(mapDBRowToScrimPost).filter((post) => Scrim.isAccepted(post)); } +/** + * Finds pending (unaccepted, uncanceled, future) scrim posts and requests + * involving any of the given users whose time overlaps [startTime, endTime]. + * Used to auto-clean conflicting availability when a scrim is scheduled. + * + * @returns posts (with their member ids, for notifying) and request ids + * (deleted silently) that should be removed + */ +export async function findPendingOverlapsForUsers({ + userIds, + startTime, + endTime, + excludePostId, +}: { + userIds: number[]; + /** window start, database timestamp (seconds) */ + startTime: number; + /** window end, database timestamp (seconds) */ + endTime: number; + excludePostId: number; +}): Promise<{ + posts: Array<{ id: number; at: number; memberIds: number[] }>; + requestIds: number[]; +}> { + if (userIds.length === 0) { + return { posts: [], requestIds: [] }; + } + + const now = dateToDatabaseTimestamp(new Date()); + + const rows = await baseFindQuery + .where("ScrimPost.canceledAt", "is", null) + .where("ScrimPost.at", ">=", now) + .where((eb) => + eb.or([ + eb.exists( + eb + .selectFrom("ScrimPostUser") + .select("ScrimPostUser.scrimPostId") + .whereRef("ScrimPostUser.scrimPostId", "=", "ScrimPost.id") + .where("ScrimPostUser.userId", "in", userIds), + ), + eb.exists( + eb + .selectFrom("ScrimPostRequest") + .innerJoin( + "ScrimPostRequestUser", + "ScrimPostRequestUser.scrimPostRequestId", + "ScrimPostRequest.id", + ) + .select("ScrimPostRequest.scrimPostId") + .whereRef("ScrimPostRequest.scrimPostId", "=", "ScrimPost.id") + .where("ScrimPostRequestUser.userId", "in", userIds), + ), + ]), + ) + .execute(); + + const userIdSet = new Set(userIds); + + const posts: Array<{ id: number; at: number; memberIds: number[] }> = []; + const requestIds: number[] = []; + + for (const post of rows + .map(mapDBRowToScrimPost) + .filter((post) => !Scrim.isAccepted(post))) { + if (post.id === excludePostId) continue; + + const postInvolvesUser = post.users.some((u) => userIdSet.has(u.id)); + const postIntervalOverlaps = + post.at <= endTime && (post.rangeEnd ?? post.at) >= startTime; + if (postInvolvesUser && postIntervalOverlaps) { + posts.push({ + id: post.id, + at: post.at, + memberIds: post.users.map((u) => u.id), + }); + } + + for (const request of post.requests) { + if (request.isAccepted) continue; + const effectiveAt = request.at ?? post.at; + const requestInvolvesUser = request.users.some((u) => + userIdSet.has(u.id), + ); + if ( + requestInvolvesUser && + effectiveAt >= startTime && + effectiveAt <= endTime + ) { + requestIds.push(request.id); + } + } + } + + return { posts, requestIds }; +} + export type SidebarScrim = { id: number; at: number; diff --git a/app/features/scrims/actions/scrims.server.ts b/app/features/scrims/actions/scrims.server.ts index 72dfa4491..21fcb24ff 100644 --- a/app/features/scrims/actions/scrims.server.ts +++ b/app/features/scrims/actions/scrims.server.ts @@ -1,4 +1,4 @@ -import { add } from "date-fns"; +import { add, sub } from "date-fns"; import type { ActionFunctionArgs } from "react-router"; import { redirect } from "react-router"; import * as AssociationsRepository from "~/features/associations/AssociationRepository.server"; @@ -14,6 +14,7 @@ import { dateToDatabaseTimestamp, } from "~/utils/dates"; import { ConcurrentModificationError } from "~/utils/errors"; +import { logger } from "~/utils/logger"; import { actionError, errorToast, @@ -24,6 +25,7 @@ import { assertUnreachable } from "~/utils/types"; import { navIconUrl, scrimPage, scrimsPage } from "~/utils/urls"; import * as Scrim from "../core/Scrim"; import * as ScrimPostRepository from "../ScrimPostRepository.server"; +import { SCRIM } from "../scrims-constants"; import { type newRequestSchema, scrimsActionSchema } from "../scrims-schemas"; import { generateTimeOptions } from "../scrims-utils"; import { usersListForPost } from "./scrims.new.server"; @@ -174,6 +176,46 @@ export const action = async ({ request }: ActionFunctionArgs) => { }, }); + if (fullPost) { + try { + const bookedAt = databaseTimestampToDate( + Scrim.getStartTime(fullPost), + ); + const startTime = dateToDatabaseTimestamp( + sub(bookedAt, { hours: SCRIM.AUTO_CANCEL_WINDOW_HOURS }), + ); + const endTime = dateToDatabaseTimestamp( + add(bookedAt, { hours: SCRIM.AUTO_CANCEL_WINDOW_HOURS }), + ); + + const { posts, requestIds } = + await ScrimPostRepository.findPendingOverlapsForUsers({ + userIds: Scrim.participantIdsListFromAccepted(fullPost), + startTime, + endTime, + excludePostId: post.id, + }); + + for (const requestId of requestIds) { + await ScrimPostRepository.deleteRequest(requestId); + } + + for (const removed of posts) { + await ScrimPostRepository.del(removed.id); + notify({ + userIds: removed.memberIds, + defaultSeenUserIds: [user.id], + notification: { + type: "SCRIM_AUTO_DELETED", + meta: { at: removed.at }, + }, + }); + } + } catch (error) { + logger.error("Failed to auto-cancel overlapping scrims", error); + } + } + break; } case "CANCEL_REQUEST": { diff --git a/app/features/scrims/components/ScrimCard.tsx b/app/features/scrims/components/ScrimCard.tsx index 945ec9fff..b47778bd3 100644 --- a/app/features/scrims/components/ScrimCard.tsx +++ b/app/features/scrims/components/ScrimCard.tsx @@ -524,6 +524,7 @@ export function ScrimRequestCard({ dialogHeading={t("scrims:acceptModal.title", { groupName: teamName, })} + description={t("scrims:autoCancelInfo")} fields={[ ["scrimPostRequestId", request.id], ["_action", "ACCEPT_REQUEST"], diff --git a/app/features/scrims/components/ScrimRequestModal.tsx b/app/features/scrims/components/ScrimRequestModal.tsx index 6475e64c2..ce78f540b 100644 --- a/app/features/scrims/components/ScrimRequestModal.tsx +++ b/app/features/scrims/components/ScrimRequestModal.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { useLoaderData } from "react-router"; import { Divider } from "~/components/Divider"; import { SendouDialog } from "~/components/elements/Dialog"; +import { FormMessage } from "~/components/FormMessage"; import type { CustomFieldRenderProps } from "~/form"; import { SendouForm } from "~/form/SendouForm"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; @@ -77,6 +78,7 @@ export function ScrimRequestModal({ ) : null} + {t("scrims:autoCancelInfo")} )} diff --git a/app/features/scrims/scrims-constants.ts b/app/features/scrims/scrims-constants.ts index 0bef4aa9b..cad03fa89 100644 --- a/app/features/scrims/scrims-constants.ts +++ b/app/features/scrims/scrims-constants.ts @@ -20,6 +20,7 @@ export const SCRIM = { REQUEST_MESSAGE_MAX_LENGTH: 200, MAX_TIME_RANGE_MS: 3 * 60 * 60 * 1000, // 3 hours ROOM_LINK_FRESHNESS_MINUTES: 30, + AUTO_CANCEL_WINDOW_HOURS: 1, }; export const SCRIM_TRACKING_AUTO_LOCK_HOURS = 4; diff --git a/e2e/scrims.spec.ts b/e2e/scrims.spec.ts index ca6f91bad..34bb7a861 100644 --- a/e2e/scrims.spec.ts +++ b/e2e/scrims.spec.ts @@ -126,6 +126,48 @@ test.describe("Scrims", () => { await expect(page.getByText("Scheduled scrim")).toBeVisible(); }); + test("auto-cancels overlapping pending scrims when a scrim is booked", async ({ + page, + }) => { + await seed(page, "NO_SCRIMS"); + await impersonate(page); + + const bookedAt = new Date(); + bookedAt.setDate(bookedAt.getDate() + 1); + bookedAt.setHours(18, 0, 0, 0); + + const overlappingAt = new Date(bookedAt); + overlappingAt.setMinutes(30); // within ±1h of the booked time + + const farAwayAt = new Date(bookedAt); + farAwayAt.setHours(22, 0, 0, 0); // outside the ±1h window + + await createScrimPost(page, bookedAt, "Booked post"); + await createScrimPost(page, overlappingAt, "Overlapping post"); + await createScrimPost(page, farAwayAt, "Far away post"); + + // NZAP requests the earliest (soon-to-be-booked) post + await impersonate(page, NZAP_TEST_ID); + await navigate({ page, url: scrimsPage() }); + await page.getByTestId("available-scrims-tab").click(); + await page.getByTestId("request-scrim-button").first().click(); + await selectUser({ labelName: "User 2", page, userName: "5" }); + await selectUser({ labelName: "User 3", page, userName: "6" }); + await selectUser({ labelName: "User 4", page, userName: "7" }); + await submit(page); + + // Author accepts the request, booking the scrim + await impersonate(page); + await navigate({ page, url: scrimsPage() }); + await page.getByTestId("confirm-modal-trigger-button").first().click(); + await submit(page, "confirm-button"); + + // The overlapping pending post is auto-removed, the far away one survives + await page.getByRole("tab", { name: /Owned/ }).click(); + await expect(page.getByText("Far away post")).toBeVisible(); + await expect(page.getByText("Overlapping post")).toHaveCount(0); + }); + test("cancels a scrim and shows canceled status", async ({ page }) => { await seed(page); await impersonate(page, ADMIN_ID); @@ -356,6 +398,17 @@ test.describe("Scrims", () => { }); }); +async function createScrimPost(page: Page, at: Date, text: string) { + await navigate({ page, url: newScrimPostPage() }); + + const form = createFormHelpers(page, scrimsNewFormSchema); + + await form.setDateTime("at", at); + await form.fill("postText", text); + + await submit(page); +} + async function reportScrimMapWinner(page: Page, winner: "ALPHA" | "BRAVO") { const testId = winner === "ALPHA" ? "winner-radio-1" : "winner-radio-2"; await expect( diff --git a/locales/da/common.json b/locales/da/common.json index 9500c9e2e..ef6ce3438 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/da/scrims.json b/locales/da/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/da/scrims.json +++ b/locales/da/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/de/common.json b/locales/de/common.json index af83f6644..b050c7b92 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/de/scrims.json b/locales/de/scrims.json index b9a1bbf6f..b7a9d2d99 100644 --- a/locales/de/scrims.json +++ b/locales/de/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "Neuen Scrim-Post erstellen", "forms.with.title": "Mit", diff --git a/locales/en/common.json b/locales/en/common.json index 9b011aa68..503cccab1 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "The scrim vs. {{opponentTeamName}} was canceled", "notifications.title.SCRIM_STARTING_SOON": "Scrim Starting Soon", "notifications.text.SCRIM_STARTING_SOON": "Your scrim vs. {{opponentTeamName}} is starting soon", + "notifications.title.SCRIM_AUTO_DELETED": "Scrim Post Removed", + "notifications.text.SCRIM_AUTO_DELETED": "Your scrim post was automatically removed because you booked a scrim around that time (± 1 hour)", "notifications.title.COMMISSIONS_CLOSED": "Commissions Closed", "notifications.text.COMMISSIONS_CLOSED": "If your commissions are still open, please re-enable them", "notifications.title.FRIEND_REQUEST_RECEIVED": "Friend Request", diff --git a/locales/en/scrims.json b/locales/en/scrims.json index 1b2c351a3..be1773d37 100644 --- a/locales/en/scrims.json +++ b/locales/en/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "Accept the request to scrim by {{groupName}} & reject others (if any)?", "acceptModal.prevented": "Ask the person who posted the scrim to accept this request", "acceptModal.confirmFor": "Confirm for {{time}}", + "autoCancelInfo": "Once a scrim is booked, any other posts and requests within ±1 hour that share players are automatically removed to avoid double-booking.", "postModal.footer": "Post created {{time}}", "forms.title": "Creating a new scrim post", "forms.with.title": "With", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index be20dfd7e..ca8752bf6 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "El scrim empieza pronto", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "Comisiones Cerradas", "notifications.text.COMMISSIONS_CLOSED": "Si tus comisiones siguen abiertas, por favor vuelve a activarlas", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/es-ES/scrims.json b/locales/es-ES/scrims.json index c14c20ad6..932ac7b17 100644 --- a/locales/es-ES/scrims.json +++ b/locales/es-ES/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "¿Aceptar la petición de scrim de {{groupName}} y rechazar las demás (si las hay)?", "acceptModal.prevented": "Pide a la persona que publicó el scrim que acepte esta petición", "acceptModal.confirmFor": "Confirmar para: {{time}}", + "autoCancelInfo": "", "postModal.footer": "Publicación creada a las {{time}}", "forms.title": "Creando una nueva publicación de scrim", "forms.with.title": "Con", diff --git a/locales/es-US/common.json b/locales/es-US/common.json index cabd37224..502c83399 100644 --- a/locales/es-US/common.json +++ b/locales/es-US/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/es-US/scrims.json b/locales/es-US/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/es-US/scrims.json +++ b/locales/es-US/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/fr-CA/common.json b/locales/fr-CA/common.json index d3bd1a76f..5d3554fed 100644 --- a/locales/fr-CA/common.json +++ b/locales/fr-CA/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/fr-CA/scrims.json b/locales/fr-CA/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/fr-CA/scrims.json +++ b/locales/fr-CA/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/fr-EU/common.json b/locales/fr-EU/common.json index 9ba1dcc5e..e45e29c5a 100644 --- a/locales/fr-EU/common.json +++ b/locales/fr-EU/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/fr-EU/scrims.json b/locales/fr-EU/scrims.json index 78490b552..d2fea2952 100644 --- a/locales/fr-EU/scrims.json +++ b/locales/fr-EU/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "Créer un nouveau post", "forms.with.title": "Avec", diff --git a/locales/he/common.json b/locales/he/common.json index d7379b7a9..a0829f1d8 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/he/scrims.json b/locales/he/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/he/scrims.json +++ b/locales/he/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/it/common.json b/locales/it/common.json index 29112b154..4ed349dd2 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/it/scrims.json b/locales/it/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/it/scrims.json +++ b/locales/it/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/ja/common.json b/locales/ja/common.json index beecaa856..a3d60acfb 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/ja/scrims.json b/locales/ja/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/ja/scrims.json +++ b/locales/ja/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/ko/common.json b/locales/ko/common.json index c32d39fd6..c3905f0ed 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/ko/scrims.json b/locales/ko/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/ko/scrims.json +++ b/locales/ko/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/nl/common.json b/locales/nl/common.json index ef78c0c57..bd49f4b2a 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/nl/scrims.json b/locales/nl/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/nl/scrims.json +++ b/locales/nl/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/pl/common.json b/locales/pl/common.json index 72e6b1084..82dc442df 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/pl/scrims.json b/locales/pl/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/pl/scrims.json +++ b/locales/pl/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 318fb6712..a1a5cd644 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/pt-BR/scrims.json b/locales/pt-BR/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/pt-BR/scrims.json +++ b/locales/pt-BR/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/ru/common.json b/locales/ru/common.json index 9ce8f9ca1..59132d2ba 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/ru/scrims.json b/locales/ru/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/ru/scrims.json +++ b/locales/ru/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", diff --git a/locales/zh/common.json b/locales/zh/common.json index 61113efc7..1d4e14381 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -84,6 +84,8 @@ "notifications.text.SCRIM_CANCELED": "", "notifications.title.SCRIM_STARTING_SOON": "", "notifications.text.SCRIM_STARTING_SOON": "", + "notifications.title.SCRIM_AUTO_DELETED": "", + "notifications.text.SCRIM_AUTO_DELETED": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "notifications.title.FRIEND_REQUEST_RECEIVED": "", diff --git a/locales/zh/scrims.json b/locales/zh/scrims.json index 98ee8007a..baf203a55 100644 --- a/locales/zh/scrims.json +++ b/locales/zh/scrims.json @@ -38,6 +38,7 @@ "acceptModal.title": "", "acceptModal.prevented": "", "acceptModal.confirmFor": "", + "autoCancelInfo": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "",