diff --git a/app/components/Avatar.tsx b/app/components/Avatar.tsx index 22d4329e1..1be4c682e 100644 --- a/app/components/Avatar.tsx +++ b/app/components/Avatar.tsx @@ -5,6 +5,7 @@ import { BLANK_IMAGE_URL, discordAvatarUrl } from "~/utils/urls"; const dimensions = { xxxs: 16, + xxxsm: 20, xxs: 24, xs: 36, sm: 44, diff --git a/app/components/elements/TournamentSearch.module.css b/app/components/elements/TournamentSearch.module.css new file mode 100644 index 000000000..1752cfaf4 --- /dev/null +++ b/app/components/elements/TournamentSearch.module.css @@ -0,0 +1,71 @@ +.item { + font-size: var(--fonts-xsm); + font-weight: var(--semi-bold); + padding: var(--s-1-5); + border-radius: var(--rounded-sm); + height: 33px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: flex; + align-items: center; + gap: var(--s-2); +} + +.popover { + min-height: 250px; +} + +.itemTextsContainer { + line-height: 1.1; +} + +.itemTextsContainer span { + max-width: 175px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: block; +} + +.selectValue { + max-width: calc(var(--select-width) - 55px); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: flex; + align-items: center; + gap: var(--s-2); +} + +button:disabled .selectValue { + color: var(--text-lighter); + font-style: italic; +} + +.placeholder { + font-size: var(--fonts-xs); + font-weight: var(--semi-bold); + color: var(--text-lighter); + text-align: center; + display: grid; + place-items: center; + height: 162px; + margin-block: var(--s-4); +} + +.itemAdditionalText { + font-size: var(--fonts-xxsm); + color: var(--text-lighter); +} + +button .itemAdditionalText { + display: none; +} + +.logo { + width: 24px; + height: 24px; + border-radius: var(--rounded-sm); + object-fit: cover; +} diff --git a/app/components/elements/TournamentSearch.tsx b/app/components/elements/TournamentSearch.tsx new file mode 100644 index 000000000..3e375f665 --- /dev/null +++ b/app/components/elements/TournamentSearch.tsx @@ -0,0 +1,224 @@ +import { useFetcher } from "@remix-run/react"; +import clsx from "clsx"; +import { format, sub } from "date-fns"; +import * as React from "react"; +import { + Autocomplete, + Button, + Input, + type Key, + ListBox, + ListBoxItem, + Popover, + SearchField, + Select, + type SelectProps, + SelectValue, +} from "react-aria-components"; +import { useTranslation } from "react-i18next"; +import { useDebounce } from "react-use"; +import { SendouBottomTexts } from "~/components/elements/BottomTexts"; +import { SendouLabel } from "~/components/elements/Label"; +import { ChevronUpDownIcon } from "~/components/icons/ChevronUpDown"; +import { CrossIcon } from "~/components/icons/Cross"; +import type { TournamentSearchLoaderData } from "~/features/tournament/routes/to.search"; +import { databaseTimestampToDate } from "~/utils/dates"; +import { SearchIcon } from "../icons/Search"; + +import selectStyles from "./Select.module.css"; +import tournamentSearchStyles from "./TournamentSearch.module.css"; + +type TournamentSearchItem = NonNullable< + Extract +>["tournaments"][number]; + +interface TournamentSearchProps + extends Omit, "children"> { + name?: string; + label?: string; + bottomText?: string; + errorText?: string; + initialTournamentId?: number; + onChange?: (tournament: TournamentSearchItem) => void; +} + +export const TournamentSearch = React.forwardRef(function TournamentSearch< + T extends object, +>( + { + name, + label, + bottomText, + errorText, + initialTournamentId, + onChange, + ...rest + }: TournamentSearchProps, + ref?: React.Ref, +) { + const [selectedKey, setSelectedKey] = React.useState( + initialTournamentId ?? null, + ); + const list = useTournamentSearch(setSelectedKey); + + const onSelectionChange = (tournamentId: number) => { + setSelectedKey(tournamentId); + const tournament = list.items.find( + (tournament) => + typeof tournament.id === "number" && tournament.id === tournamentId, + ); + if (tournament && typeof tournament.id === "number") { + onChange?.(tournament as TournamentSearchItem); + } + }; + + return ( + + + + tournament !== undefined)} + className={selectStyles.listBox} + > + {(item) => } + + + + + ); +}); + +function TournamentItem({ + item, +}: { + item: + | TournamentSearchItem + | { + id: "NO_RESULTS"; + } + | { + id: "PLACEHOLDER"; + }; +}) { + const { t } = useTranslation(["common"]); + + if (typeof item.id === "string") { + return ( + + {item.id === "PLACEHOLDER" + ? t("common:forms.tournamentSearch.placeholder") + : t("common:forms.tournamentSearch.noResults")} + + ); + } + + const additionalText = () => { + const date = databaseTimestampToDate(item.startTime); + return format(date, "MMM d, yyyy"); + }; + + return ( + + clsx(tournamentSearchStyles.item, { + [selectStyles.itemFocused]: isFocused, + [selectStyles.itemSelected]: isSelected, + }) + } + data-testid="tournament-search-item" + > + +
+ {item.name} + {additionalText() ? ( +
+ {additionalText()} +
+ ) : null} +
+
+ ); +} + +function useTournamentSearch( + setSelectedKey: (tournamentId: number | null) => void, +) { + const [filterText, setFilterText] = React.useState(""); + + const queryFetcher = useFetcher(); + + useDebounce( + () => { + if (!filterText) return; + queryFetcher.load( + `/to/search?q=${filterText}&limit=6&minStartTime=${sub(new Date(), { days: 7 }).toISOString()}`, + ); + setSelectedKey(null); + }, + 500, + [filterText], + ); + + const items = () => { + if ( + queryFetcher.data && + !Array.isArray(queryFetcher.data) && + queryFetcher.data.query === filterText + ) { + if (queryFetcher.data.tournaments.length === 0) { + return [{ id: "NO_RESULTS" }]; + } + return queryFetcher.data.tournaments; + } + + return [{ id: "PLACEHOLDER" }]; + }; + + return { + filterText, + setFilterText, + items: items(), + }; +} diff --git a/app/components/layout/AnythingAdder.tsx b/app/components/layout/AnythingAdder.tsx index f56364111..8021b9f40 100644 --- a/app/components/layout/AnythingAdder.tsx +++ b/app/components/layout/AnythingAdder.tsx @@ -1,7 +1,6 @@ import { Button } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { useUser } from "~/features/auth/core/user"; -import { FF_SCRIMS_ENABLED } from "~/features/scrims/scrims-constants"; import { CALENDAR_NEW_PAGE, lfgNewPostPage, @@ -62,22 +61,18 @@ export function AnythingAdder() { imagePath: navIconUrl("t"), href: NEW_TEAM_PAGE, }, - FF_SCRIMS_ENABLED - ? { - id: "scrimPost", - children: t("header.adder.scrimPost"), - imagePath: navIconUrl("scrims"), - href: newScrimPostPage(), - } - : null, - FF_SCRIMS_ENABLED - ? { - id: "association", - children: t("header.adder.association"), - imagePath: navIconUrl("associations"), - href: newAssociationsPage(), - } - : null, + { + id: "scrimPost", + children: t("header.adder.scrimPost"), + imagePath: navIconUrl("scrims"), + href: newScrimPostPage(), + }, + { + id: "association", + children: t("header.adder.association"), + imagePath: navIconUrl("associations"), + href: newAssociationsPage(), + }, { id: "lfgPost", children: t("header.adder.lfgPost"), diff --git a/app/components/layout/nav-items.ts b/app/components/layout/nav-items.ts index 44aa4ad02..85c2e9cc2 100644 --- a/app/components/layout/nav-items.ts +++ b/app/components/layout/nav-items.ts @@ -1,5 +1,3 @@ -import { FF_SCRIMS_ENABLED } from "~/features/scrims/scrims-constants"; - export const navItems = [ { name: "settings", @@ -38,13 +36,11 @@ export const navItems = [ url: "leaderboards", prefetch: false, }, - FF_SCRIMS_ENABLED - ? { - name: "scrims", - url: "scrims", - prefetch: false, - } - : null, + { + name: "scrims", + url: "scrims", + prefetch: false, + }, { name: "lfg", url: "lfg", diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 129251da5..d45d60d9b 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -67,7 +67,11 @@ import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids"; import { SENDOUQ_DEFAULT_MAPS } from "~/modules/tournament-map-list-generator/constants"; import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator/types"; import { nullFilledArray } from "~/utils/arrays"; -import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; +import { + databaseTimestampNow, + databaseTimestampToDate, + dateToDatabaseTimestamp, +} from "~/utils/dates"; import { shortNanoid } from "~/utils/id"; import invariant from "~/utils/invariant"; import { mySlugify } from "~/utils/urls"; @@ -2354,6 +2358,10 @@ async function scrimPosts() { return { maxDiv, minDiv }; }; + const maps = (): "SZ" | "ALL" | "RANKED" | null => { + return faker.helpers.arrayElement(["SZ", "ALL", "RANKED", null, null]); + }; + const users = () => { const count = faker.helpers.arrayElement([4, 4, 4, 4, 4, 4, 5, 5, 5, 6]); @@ -2372,8 +2380,17 @@ async function scrimPosts() { for (let i = 0; i < 20; i++) { const divs = divRange(); + const atTime = date(); + const hasRangeEnd = Math.random() > 0.5; await ScrimPostRepository.insert({ - at: date(), + at: atTime, + rangeEnd: hasRangeEnd + ? dateToDatabaseTimestamp( + add(databaseTimestampToDate(atTime), { + hours: faker.helpers.rangeToNumber({ min: 1, max: 3 }), + }), + ) + : null, isScheduledForFuture: true, maxDiv: divs?.maxDiv, minDiv: divs?.minDiv, @@ -2385,11 +2402,14 @@ async function scrimPosts() { visibility: null, users: users(), managedByAnyone: true, + maps: maps(), + mapsTournamentId: null, }); } + const adminPostAtTime = date(true); // admin's scrim is always at least 1 hour in the future const adminPostId = await ScrimPostRepository.insert({ - at: date(true), // admin's scrim is always at least 1 hour in the future + at: adminPostAtTime, isScheduledForFuture: true, text: faker.number.float(1) > 0.5 @@ -2400,14 +2420,24 @@ async function scrimPosts() { .map((u) => ({ ...u, isOwner: 0 })) .concat({ userId: ADMIN_ID, isOwner: 1 }), managedByAnyone: true, + maps: maps(), + mapsTournamentId: null, }); await ScrimPostRepository.insertRequest({ scrimPostId: adminPostId, users: users(), + message: + faker.number.float(1) > 0.5 + ? faker.lorem.sentence({ min: 5, max: 15 }) + : null, }); await ScrimPostRepository.insertRequest({ scrimPostId: adminPostId, users: users(), + message: + faker.number.float(1) > 0.5 + ? faker.lorem.sentence({ min: 5, max: 15 }) + : null, }); } @@ -2426,6 +2456,10 @@ async function scrimPostRequests() { isOwner: member.userId === ADMIN_ID ? 1 : 0, })), teamId: 1, + message: + faker.number.float(1) > 0.5 + ? faker.lorem.sentence({ min: 5, max: 15 }) + : null, }); } diff --git a/app/db/tables.ts b/app/db/tables.ts index c25e94342..07bbb2197 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -11,6 +11,7 @@ import type { tags } from "~/features/calendar/calendar-constants"; import type { CalendarFilters } from "~/features/calendar/calendar-types"; import type { TieredSkill } from "~/features/mmr/tiered.server"; import type { Notification as NotificationValue } from "~/features/notifications/notifications-types"; +import type { ScrimFilters } from "~/features/scrims/scrims-types"; import type { TEAM_MEMBER_ROLES } from "~/features/team/team-constants"; import type * as PickBan from "~/features/tournament-bracket/core/PickBan"; import type * as Progression from "~/features/tournament-bracket/core/Progression"; @@ -681,7 +682,6 @@ export interface TournamentTeam { inviteCode: string; name: string; prefersNotToHost: Generated; - noScreen: Generated; droppedOut: Generated; seed: number | null; /** For formats that have many starting brackets, where should the team start? */ @@ -828,6 +828,7 @@ export interface UserPreferences { disableBuildAbilitySorting?: boolean; disallowScrimPickupsFromUntrusted?: boolean; defaultCalendarFilters?: CalendarFilters; + defaultScrimsFilters?: ScrimFilters; } export interface User { @@ -975,6 +976,8 @@ export interface ScrimPost { id: GeneratedAlways; /** When is the scrim scheduled to happen */ at: number; + /** Optional end of time range indicating team accepts scrims starting between at and rangeEnd */ + rangeEnd: number | null; /** Highest LUTI div accepted */ maxDiv: number | null; /** Lowest LUTI div accepted */ @@ -997,6 +1000,10 @@ export interface ScrimPost { cancelReason: string | null; /** When the post was made was it scheduled for a future time slot (as opposed to looking now) */ isScheduledForFuture: Generated; + /** Maps/modes the scrim is available for. If null means no preference unless "mapsTournamentId" is set */ + maps: "SZ" | "ALL" | "RANKED" | null; + /** If set, specifies the maps of a tournament to play */ + mapsTournamentId: number | null; createdAt: GeneratedAlways; updatedAt: Generated; } @@ -1012,6 +1019,9 @@ export interface ScrimPostRequest { id: GeneratedAlways; scrimPostId: number; teamId: number | null; + message: string | null; + /** Specific time selected by requester (required when post has rangeEnd) */ + at: number | null; isAccepted: Generated; createdAt: GeneratedAlways; } diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index b5e29b0cd..e8f9ffabc 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -846,8 +846,7 @@ function EnableNoScreenToggle() { onChange={setEnableNoScreen} /> - When registering ask teams if they want to play without Splattercolor - Screen. + Ban Splattercolor Screen in matches depending on the teams' preferences. ); diff --git a/app/features/info/routes/support.tsx b/app/features/info/routes/support.tsx index ba9271d5f..0e5af9fad 100644 --- a/app/features/info/routes/support.tsx +++ b/app/features/info/routes/support.tsx @@ -5,7 +5,6 @@ import { Badge } from "~/components/Badge"; import { LinkButton } from "~/components/elements/Button"; import { CheckmarkIcon } from "~/components/icons/Checkmark"; import { Main } from "~/components/Main"; -import { FF_SCRIMS_ENABLED } from "~/features/scrims/scrims-constants"; import { metaTags } from "~/utils/remix"; import { PATREON_HOW_TO_CONNECT_DISCORD_URL, @@ -169,9 +168,7 @@ function SupportTable() {
Support
Supporter
Supporter+
- {PERKS.filter( - (perk) => FF_SCRIMS_ENABLED || perk.name !== "joinMoreAssociations", - ).map((perk) => { + {PERKS.map((perk) => { return (
diff --git a/app/features/notifications/notifications-types.ts b/app/features/notifications/notifications-types.ts index 690f59f26..56496ff94 100644 --- a/app/features/notifications/notifications-types.ts +++ b/app/features/notifications/notifications-types.ts @@ -64,6 +64,7 @@ export type Notification = | NotificationItem<"SCRIM_NEW_REQUEST", { fromUsername: string }> | NotificationItem<"SCRIM_SCHEDULED", { id: number; at: number }> | NotificationItem<"SCRIM_CANCELED", { id: number; at: number }> + | NotificationItem<"SCRIM_STARTING_SOON", { id: number; at: number }> | NotificationItem<"COMMISSIONS_CLOSED", { discordId: string }>; type NotificationItem< diff --git a/app/features/notifications/notifications-utils.ts b/app/features/notifications/notifications-utils.ts index 912f6d71d..9c3b541a9 100644 --- a/app/features/notifications/notifications-utils.ts +++ b/app/features/notifications/notifications-utils.ts @@ -38,6 +38,7 @@ export const notificationNavIcon = (type: Notification["type"]) => { case "SCRIM_NEW_REQUEST": case "SCRIM_SCHEDULED": case "SCRIM_CANCELED": + case "SCRIM_STARTING_SOON": return "scrims"; default: assertUnreachable(type); @@ -82,7 +83,8 @@ export const notificationLink = (notification: Notification) => { return scrimsPage(); } case "SCRIM_CANCELED": - case "SCRIM_SCHEDULED": { + case "SCRIM_SCHEDULED": + case "SCRIM_STARTING_SOON": { return scrimPage(notification.meta.id); } case "COMMISSIONS_CLOSED": { @@ -100,7 +102,8 @@ export const mapMetaForTranslation = ( ) => { if ( notification.type === "SCRIM_SCHEDULED" || - notification.type === "SCRIM_CANCELED" + notification.type === "SCRIM_CANCELED" || + notification.type === "SCRIM_STARTING_SOON" ) { return { ...notification.meta, diff --git a/app/features/scrims/ScrimPostRepository.server.ts b/app/features/scrims/ScrimPostRepository.server.ts index e9e03b514..dbc7e5d11 100644 --- a/app/features/scrims/ScrimPostRepository.server.ts +++ b/app/features/scrims/ScrimPostRepository.server.ts @@ -5,17 +5,26 @@ import type { Tables, TablesInsertable } from "~/db/tables"; import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; import { shortNanoid } from "~/utils/id"; import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; +import { userSubmittedImage } from "~/utils/urls-img"; import { db } from "../../db/sql"; import invariant from "../../utils/invariant"; import type { Unwrapped } from "../../utils/types"; import type { AssociationVisibility } from "../associations/associations-types"; +import { HACKY_resolvePicture } from "../tournament/tournament-utils"; import * as Scrim from "./core/Scrim"; import type { ScrimPost, ScrimPostUser } from "./scrims-types"; import { getPostRequestCensor, parseLutiDiv } from "./scrims-utils"; type InsertArgs = Pick< TablesInsertable["ScrimPost"], - "at" | "maxDiv" | "minDiv" | "teamId" | "text" + | "at" + | "rangeEnd" + | "maxDiv" + | "minDiv" + | "teamId" + | "text" + | "maps" + | "mapsTournamentId" > & { /** users related to the post other than the author */ users: Array, "userId" | "isOwner">>; @@ -34,10 +43,13 @@ export function insert(args: InsertArgs) { .insertInto("ScrimPost") .values({ at: args.at, + rangeEnd: args.rangeEnd, maxDiv: args.maxDiv, minDiv: args.minDiv, teamId: args.teamId, text: args.text, + maps: args.maps, + mapsTournamentId: args.mapsTournamentId, visibility: args.visibility ? JSON.stringify(args.visibility) : null, chatCode: shortNanoid(), managedByAnyone: args.managedByAnyone ? 1 : 0, @@ -57,7 +69,7 @@ export function insert(args: InsertArgs) { type InsertRequestArgs = Pick< Insertable, - "scrimPostId" | "teamId" + "scrimPostId" | "teamId" | "message" | "at" > & { users: Array< Pick, "userId" | "isOwner"> @@ -73,6 +85,8 @@ export function insertRequest(args: InsertRequestArgs) { .values({ scrimPostId: args.scrimPostId, teamId: args.teamId, + message: args.message, + at: args.at, }) .returning("id") .executeTakeFirstOrThrow(); @@ -98,14 +112,27 @@ const baseFindQuery = db .selectFrom("ScrimPost") .leftJoin("Team", "ScrimPost.teamId", "Team.id") .leftJoin("UserSubmittedImage", "Team.avatarImgId", "UserSubmittedImage.id") + .leftJoin( + "CalendarEvent", + "ScrimPost.mapsTournamentId", + "CalendarEvent.tournamentId", + ) + .leftJoin( + "UserSubmittedImage as TournamentAvatar", + "CalendarEvent.avatarImgId", + "TournamentAvatar.id", + ) .select((eb) => [ "ScrimPost.id", "ScrimPost.at", + "ScrimPost.rangeEnd", "ScrimPost.createdAt", "ScrimPost.visibility", "ScrimPost.maxDiv", "ScrimPost.minDiv", "ScrimPost.text", + "ScrimPost.maps", + "ScrimPost.mapsTournamentId", "ScrimPost.managedByAnyone", "ScrimPost.canceledAt", "ScrimPost.canceledByUserId", @@ -116,6 +143,11 @@ const baseFindQuery = db customUrl: eb.ref("Team.customUrl"), avatarUrl: eb.ref("UserSubmittedImage.url"), }).as("team"), + jsonBuildObject({ + id: eb.ref("CalendarEvent.tournamentId"), + name: eb.ref("CalendarEvent.name"), + avatarUrl: eb.ref("TournamentAvatar.url"), + }).as("mapsTournament"), jsonArrayFrom( eb .selectFrom("ScrimPostUser") @@ -136,6 +168,8 @@ const baseFindQuery = db "ScrimPostRequest.id", "ScrimPostRequest.isAccepted", "ScrimPostRequest.createdAt", + "ScrimPostRequest.message", + "ScrimPostRequest.at", jsonBuildObject({ name: innerEb.ref("Team.name"), customUrl: innerEb.ref("Team.customUrl"), @@ -207,9 +241,10 @@ const mapDBRowToScrimPost = ( } } - return { + const result = { id: row.id, at: row.at, + rangeEnd: row.rangeEnd, createdAt: row.createdAt, visibility: row.visibility, text: row.text, @@ -218,6 +253,16 @@ const mapDBRowToScrimPost = ( typeof row.maxDiv === "number" && typeof row.minDiv === "number" ? { max: parseLutiDiv(row.maxDiv), min: parseLutiDiv(row.minDiv) } : null, + maps: row.maps, + mapsTournament: row.mapsTournament.id + ? { + id: row.mapsTournament.id, + name: row.mapsTournament.name!, + avatarUrl: row.mapsTournament.avatarUrl + ? userSubmittedImage(row.mapsTournament.avatarUrl) + : HACKY_resolvePicture({ name: row.mapsTournament.name! }), + } + : null, chatCode: row.chatCode ?? null, team: row.team.name ? { @@ -231,6 +276,8 @@ const mapDBRowToScrimPost = ( id: request.id, isAccepted: Boolean(request.isAccepted), createdAt: request.createdAt, + message: request.message, + at: request.at, team: request.team.name ? { name: request.team.name, @@ -256,6 +303,16 @@ const mapDBRowToScrimPost = ( managedByAnyone: Boolean(row.managedByAnyone), canceled, }; + + if (!Scrim.isAccepted(result)) { + return result; + } + + return { + ...result, + at: Scrim.getStartTime(result), + rangeEnd: null, + }; }; export async function findById(scrimPostId: number): Promise { @@ -315,3 +372,34 @@ export async function cancelScrim( .where("canceledAt", "is", null) .execute(); } + +/** + * Finds all accepted scrims scheduled within a specific time range. + * + * @returns Array of accepted (matched) scrim posts within the time range + */ +export async function findAcceptedScrimsBetweenTwoTimestamps({ + /** The earliest scrim start time to include (inclusive) */ + startTime, + /** The latest scrim start time to include (exclusive) */ + endTime, + /** Exclude scrims created after this timestamp */ + excludeRecentlyCreated, +}: { + startTime: Date; + endTime: Date; + excludeRecentlyCreated: Date; +}) { + const rows = await baseFindQuery + .where("ScrimPost.at", ">=", dateToDatabaseTimestamp(startTime)) + .where("ScrimPost.at", "<", dateToDatabaseTimestamp(endTime)) + .where("ScrimPost.canceledAt", "is", null) + .where( + "ScrimPost.createdAt", + "<", + dateToDatabaseTimestamp(excludeRecentlyCreated), + ) + .execute(); + + return rows.map(mapDBRowToScrimPost).filter((post) => Scrim.isAccepted(post)); +} diff --git a/app/features/scrims/actions/scrims.$id.server.ts b/app/features/scrims/actions/scrims.$id.server.ts index 9f7c95b00..b33ae334f 100644 --- a/app/features/scrims/actions/scrims.$id.server.ts +++ b/app/features/scrims/actions/scrims.$id.server.ts @@ -29,7 +29,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { requirePermission(post, "CANCEL", user); - if (databaseTimestampToDate(post.at) < new Date()) { + if (databaseTimestampToDate(Scrim.getStartTime(post)) < new Date()) { errorToast("Cannot cancel a scrim that was already scheduled to start"); } @@ -45,7 +45,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { type: "SCRIM_CANCELED", meta: { id: post.id, - at: databaseTimestampToJavascriptTimestamp(post.at), + at: databaseTimestampToJavascriptTimestamp(Scrim.getStartTime(post)), }, }, }); diff --git a/app/features/scrims/actions/scrims.new.server.ts b/app/features/scrims/actions/scrims.new.server.ts index df8c3672d..61abf4056 100644 --- a/app/features/scrims/actions/scrims.new.server.ts +++ b/app/features/scrims/actions/scrims.new.server.ts @@ -51,10 +51,16 @@ export const action = async ({ request }: ActionFunctionArgs) => { await ScrimPostRepository.insert({ at: dateToDatabaseTimestamp(data.at), + rangeEnd: data.rangeEnd ? dateToDatabaseTimestamp(data.rangeEnd) : null, maxDiv: data.divs ? serializeLutiDiv(data.divs.max!) : null, minDiv: data.divs ? serializeLutiDiv(data.divs.min!) : null, text: data.postText, managedByAnyone: data.managedByAnyone, + maps: + data.maps === "NO_PREFERENCE" || data.maps === "TOURNAMENT" + ? null + : data.maps, + mapsTournamentId: data.mapsTournamentId, isScheduledForFuture: data.at > // 10 minutes is an arbitrary threshold diff --git a/app/features/scrims/actions/scrims.server.ts b/app/features/scrims/actions/scrims.server.ts index a4279f49f..11f844172 100644 --- a/app/features/scrims/actions/scrims.server.ts +++ b/app/features/scrims/actions/scrims.server.ts @@ -1,16 +1,29 @@ import type { ActionFunctionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; import { requireUser } from "~/features/auth/core/user.server"; import { notify } from "~/features/notifications/core/notify.server"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; import { requirePermission } from "~/modules/permissions/guards.server"; -import { databaseTimestampToJavascriptTimestamp } from "~/utils/dates"; -import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server"; +import { + databaseTimestampToDate, + databaseTimestampToJavascriptTimestamp, + dateToDatabaseTimestamp, +} from "~/utils/dates"; +import { + actionError, + errorToastIfFalsy, + parseRequestPayload, +} from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; +import { scrimsPage } from "~/utils/urls"; import * as ScrimPostRepository from "../ScrimPostRepository.server"; -import { scrimsActionSchema } from "../scrims-schemas"; +import { type newRequestSchema, scrimsActionSchema } from "../scrims-schemas"; +import { generateTimeOptions } from "../scrims-utils"; import { usersListForPost } from "./scrims.new.server"; export const action = async ({ request }: ActionFunctionArgs) => { const user = await requireUser(request); + const data = await parseRequestPayload({ request, schema: scrimsActionSchema, @@ -33,9 +46,33 @@ export const action = async ({ request }: ActionFunctionArgs) => { postId: data.scrimPostId, }); + if (post.rangeEnd && !data.at) { + return actionError({ + msg: "Please select a time for the scrim", + field: "at", + }); + } + + if (post.rangeEnd && data.at) { + const validTimeOptions = generateTimeOptions( + databaseTimestampToDate(post.at), + databaseTimestampToDate(post.rangeEnd), + ); + const requestTime = data.at.getTime(); + + if (!validTimeOptions.includes(requestTime)) { + return actionError({ + msg: "Selected time must be one of the available options", + field: "at", + }); + } + } + await ScrimPostRepository.insertRequest({ scrimPostId: data.scrimPostId, teamId: data.from.mode === "TEAM" ? data.from.teamId : null, + message: data.message, + at: data.at ? dateToDatabaseTimestamp(data.at) : null, users: ( await usersListForPost({ authorId: user.id, from: data.from }) ).map((userId) => ({ @@ -79,7 +116,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { type: "SCRIM_SCHEDULED", meta: { id: post.id, - at: databaseTimestampToJavascriptTimestamp(post.at), + at: databaseTimestampToJavascriptTimestamp(request.at ?? post.at), }, }, }); @@ -102,6 +139,13 @@ export const action = async ({ request }: ActionFunctionArgs) => { break; } + case "PERSIST_SCRIM_FILTERS": { + await UserRepository.updatePreferences(user.id, { + defaultScrimsFilters: data.filters, + }); + + return redirect(scrimsPage()); + } default: { assertUnreachable(data); } diff --git a/app/features/scrims/components/LutiDivsFormField.tsx b/app/features/scrims/components/LutiDivsFormField.tsx new file mode 100644 index 000000000..c10414ec0 --- /dev/null +++ b/app/features/scrims/components/LutiDivsFormField.tsx @@ -0,0 +1,104 @@ +import type * as React from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Label } from "~/components/Label"; +import { FormMessage } from "../../../components/FormMessage"; +import { LUTI_DIVS } from "../scrims-constants"; +import type { LutiDiv } from "../scrims-types"; + +export function LutiDivsFormField() { + const methods = useFormContext(); + + const error = methods.formState.errors.divs; + + return ( +
+ ( + + )} + /> + + {error && ( + {error.message as string} + )} +
+ ); +} + +type LutiDivEdit = { + max: LutiDiv | null; + min: LutiDiv | null; +}; + +function LutiDivsSelector({ + value, + onChange, + onBlur, +}: { + value: LutiDivEdit | null; + onChange: (value: LutiDivEdit | null) => void; + onBlur: () => void; +}) { + const { t } = useTranslation(["scrims"]); + + const onChangeMin = (e: React.ChangeEvent) => { + const newValue = e.target.value === "" ? null : (e.target.value as LutiDiv); + + onChange( + newValue || value?.max + ? { min: newValue, max: value?.max ?? null } + : null, + ); + }; + + const onChangeMax = (e: React.ChangeEvent) => { + const newValue = e.target.value === "" ? null : (e.target.value as LutiDiv); + + onChange( + newValue || value?.min + ? { max: newValue, min: value?.min ?? null } + : null, + ); + }; + + return ( +
+
+ + +
+ +
+ + +
+
+ ); +} diff --git a/app/features/scrims/components/ScrimCard.module.css b/app/features/scrims/components/ScrimCard.module.css new file mode 100644 index 000000000..5b3f913e3 --- /dev/null +++ b/app/features/scrims/components/ScrimCard.module.css @@ -0,0 +1,148 @@ +.card { + border: 1px solid var(--border); + border-radius: var(--rounded); + overflow: hidden; + background-color: var(--bg-darker); + display: flex; + flex-direction: column; +} + +.header { + display: flex; + align-items: center; + gap: var(--s-3); + padding: var(--s-4); + padding-bottom: var(--s-3); +} + +.avatarContainer { + flex-shrink: 0; +} + +.teamName { + flex: 1; + font-size: var(--text-lg); + font-weight: var(--semi-bold); + margin: 0; + display: flex; + flex-direction: column; + gap: var(--s-0-5); + line-height: 1.2; +} + +.pickupLabel { + font-size: var(--fonts-xxxs); + font-weight: var(--bold); + text-transform: uppercase; + color: var(--text-lighter); +} + +.rightIconsContainer { + flex-shrink: 0; + align-self: flex-start; + display: flex; + gap: var(--s-2); +} + +.usersIcon { + width: 18px; + height: 18px; +} + +.infoRow { + display: flex; + gap: var(--s-6); + padding-inline: var(--s-4); + padding-bottom: var(--s-3); + flex-wrap: wrap; +} + +.infoItem { + display: flex; + flex-direction: column; + gap: var(--s-0-5); +} + +.infoLabel { + text-transform: uppercase; + color: var(--text-lighter); + font-weight: var(--bold); + font-size: var(--fonts-xxs); +} + +.infoValue { + font-weight: var(--semi-bold); + display: flex; + align-items: center; + gap: var(--s-1); + font-size: var(--fonts-xs); +} + +.textContent { + padding-inline: var(--s-4); + padding-bottom: var(--s-3); + font-size: var(--fonts-sm); + line-height: 1.4; + display: flex; + flex-direction: column; + gap: var(--s-0-5); +} + +.expandButton { + background: none; + border: none; + color: var(--theme); + cursor: pointer; + font-size: var(--fonts-xxs); + font-weight: var(--semi-bold); + padding: 0; + text-align: left; + text-decoration: underline; +} + +.expandButton:hover { + opacity: 0.8; +} + +.footer { + background-color: var(--bg-lighter); + padding: var(--s-2) var(--s-4); + display: flex; + justify-content: center; + margin-block-start: auto; +} + +.requestCard { + background-color: var(--theme-transparent); +} + +.requestFooter { + background-image: repeating-linear-gradient( + 45deg, + var(--bg-darker), + var(--bg-darker) 10px, + var(--bg-lighter) 10px, + var(--bg-lighter) 20px + ); +} + +.filteredFooter { + background-color: var(--theme-transparent); +} + +.canceledContainer { + display: flex; + flex-direction: column; + gap: var(--s-0-5); +} + +.strikethrough button { + text-decoration: line-through; +} + +.canceledLabel { + color: var(--theme-error); + font-size: var(--fonts-xxs); + font-weight: var(--bold); + text-transform: uppercase; +} diff --git a/app/features/scrims/components/ScrimCard.tsx b/app/features/scrims/components/ScrimCard.tsx new file mode 100644 index 000000000..73104955e --- /dev/null +++ b/app/features/scrims/components/ScrimCard.tsx @@ -0,0 +1,567 @@ +import { Form, Link } from "@remix-run/react"; +import clsx from "clsx"; +import { formatDistance } from "date-fns"; +import type React from "react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Avatar } from "~/components/Avatar"; +import { LinkButton, SendouButton } from "~/components/elements/Button"; +import { SendouDialog } from "~/components/elements/Dialog"; +import { SendouPopover } from "~/components/elements/Popover"; +import { FormWithConfirm } from "~/components/FormWithConfirm"; +import { ModeImage } from "~/components/Image"; +import { ArrowDownOnSquareIcon } from "~/components/icons/ArrowDownOnSquare"; +import { ArrowUpOnSquareIcon } from "~/components/icons/ArrowUpOnSquare"; +import { CheckmarkIcon } from "~/components/icons/Checkmark"; +import { EyeSlashIcon } from "~/components/icons/EyeSlash"; +import { SpeechBubbleFilledIcon } from "~/components/icons/SpeechBubbleFilled"; +import { TrashIcon } from "~/components/icons/Trash"; +import { UsersIcon } from "~/components/icons/Users"; +import TimePopover from "~/components/TimePopover"; +import { useUser } from "~/features/auth/core/user"; +import type { ModeShort } from "~/modules/in-game-lists/types"; +import { databaseTimestampToDate } from "~/utils/dates"; +import { scrimPage, tournamentRegisterPage, userPage } from "~/utils/urls"; +import { userSubmittedImage } from "~/utils/urls-img"; +import type { ScrimPost, ScrimPostRequest } from "../scrims-types"; +import { formatFlexTimeDisplay } from "../scrims-utils"; +import styles from "./ScrimCard.module.css"; +import { ScrimRequestModal } from "./ScrimRequestModal"; + +interface ScrimPostCardProps { + post: ScrimPost; + action?: "DELETE" | "REQUEST" | "VIEW_REQUEST" | "CONTACT"; + isFilteredOut?: boolean; +} + +export function ScrimPostCard({ + post, + action, + isFilteredOut, +}: ScrimPostCardProps) { + const { t } = useTranslation(["scrims"]); + + const owner = post.users.find((user) => user.isOwner) ?? post.users[0]; + const isPickup = !post.team?.name; + const teamName = post.team?.name ?? owner.username; + + const flexTimeDisplay = post.rangeEnd + ? formatFlexTimeDisplay(post.at, post.rangeEnd) + : null; + + return ( +
+
+
+ +
+

+ {isPickup ? ( + <> + {t("scrims:pickupBy")} + {owner.username} + + ) : ( + teamName + )} +

+
+ {post.isPrivate ? : null} + +
+
+ +
+ + + + + {flexTimeDisplay ? ( + {flexTimeDisplay} + ) : null} + {post.divs ? ( + + {post.divs.max === post.divs.min + ? post.divs.max + : `${post.divs.min}-${post.divs.max}`} + + ) : null} + + {post.maps || post.mapsTournament ? ( + + {post.mapsTournament ? ( + + ) : ( + getModesList(post.maps!).map((mode) => ( + + )) + )} + + ) : null} +
+ + {post.text ? : null} + +
+ +
+
+ ); +} + +function getModesList(maps: string): ModeShort[] { + if (maps === "SZ") { + return ["SZ"]; + } + if (maps === "RANKED") { + return ["SZ", "TC", "RM", "CB"]; + } + return ["TW", "SZ", "TC", "RM", "CB"]; +} + +function ScrimTeamAvatar({ + teamAvatarUrl, + teamName, + owner, +}: { + teamAvatarUrl: string | null | undefined; + teamName: string; + owner: ScrimPost["users"][number]; +}) { + if (teamAvatarUrl) { + return ( + + ); + } + + return ; +} + +function ScrimVisibilityPopover() { + const { t } = useTranslation(["scrims"]); + return ( + } + data-testid="limited-visibility-popover" + /> + } + > + {t("scrims:limitedVisibility")} + + ); +} + +function ScrimTeamMembersPopover({ users }: { users: ScrimPost["users"] }) { + return ( + } + /> + } + > +
+ {users.map((user) => ( + + + {user.username} + + ))} +
+
+ ); +} + +function ScrimTournamentPopover({ + tournament, +}: { + tournament: NonNullable; +}) { + return ( + + + + } + > +
+ + {tournament.name} + +
+
+ ); +} + +function ScrimStartTimeDisplay({ + isScheduledForFuture, + startTimestamp, + createdAtTimestamp, + canceled, +}: { + isScheduledForFuture: boolean; + startTimestamp: number; + createdAtTimestamp: number; + canceled: ScrimPost["canceled"]; +}) { + const { t } = useTranslation(["scrims"]); + + if (!isScheduledForFuture) { + return canceled ? ( +
+ {t("scrims:now")} + Canceled +
+ ) : ( + t("scrims:now") + ); + } + + const startTime = databaseTimestampToDate(startTimestamp); + const timePopoverFooterText = t("scrims:postModal.footer", { + time: formatDistance( + databaseTimestampToDate(createdAtTimestamp), + new Date(), + { + addSuffix: true, + }, + ), + }); + + const timeDisplay = ( + + ); + + return canceled ? ( +
+ {timeDisplay} + Canceled +
+ ) : ( + timeDisplay + ); +} + +function ScrimInfoItem({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +function ScrimExpandableText({ + text, + maxBeforeTruncate = 50, +}: { + text: string; + maxBeforeTruncate?: number; +}) { + const { t } = useTranslation(["common"]); + const [isExpanded, setIsExpanded] = useState(false); + + const shouldTruncate = text.length > maxBeforeTruncate; + const displayText = + shouldTruncate && !isExpanded + ? `${text.slice(0, maxBeforeTruncate)}...` + : text; + + return ( +
+ {displayText} + {shouldTruncate ? ( + + ) : null} +
+ ); +} + +function ScrimActionButtons({ + action, + post, +}: { + action: ScrimPostCardProps["action"]; + post: ScrimPost; +}) { + const { t, i18n } = useTranslation(["scrims", "common"]); + const user = useUser(); + const [isRequestModalOpen, setIsRequestModalOpen] = useState(false); + const [isViewRequestModalOpen, setIsViewRequestModalOpen] = useState(false); + + if (!action) { + return null; + } + + if (action === "REQUEST") { + return ( + <> + setIsRequestModalOpen(true)} + icon={} + data-testid="request-scrim-button" + > + {t("scrims:actions.request")} + + {isRequestModalOpen ? ( + setIsRequestModalOpen(false)} + /> + ) : null} + + ); + } + + if (action === "VIEW_REQUEST") { + const userRequest = post.requests.find((request) => + request.users.some((rUser) => user?.id === rUser.id), + ); + + return ( + <> + setIsViewRequestModalOpen(true)} + variant="outlined" + icon={} + data-testid="view-request-button" + > + {t("scrims:actions.viewRequest")} + + {isViewRequestModalOpen && userRequest ? ( + setIsViewRequestModalOpen(false)} + > +
+ {userRequest.message ? ( +
+
+ {t("scrims:requestModal.message.label")} +
+
{userRequest.message}
+
+ ) : null} + {userRequest.at ? ( +
+
+ {t("scrims:requestModal.at.label")} +
+
+ {databaseTimestampToDate(userRequest.at).toLocaleString( + i18n.language, + { + hour: "numeric", + minute: "2-digit", + day: "numeric", + month: "long", + }, + )} +
+
+ ) : null} +
+ + + } + > + {t("common:actions.cancel")} + +
+
+
+ ) : null} + + ); + } + + if (action === "CONTACT") { + return ( + } + > + {t("scrims:actions.contact")} + + ); + } + + return ( + + }> + {t("common:actions.delete")} + + + ); +} + +interface ScrimRequestCardProps { + request: ScrimPostRequest; + postStartTime: number; + canAccept: boolean; + showFooter?: boolean; +} + +export function ScrimRequestCard({ + request, + postStartTime, + canAccept, + showFooter = true, +}: ScrimRequestCardProps) { + const { t, i18n } = useTranslation(["scrims", "common"]); + + const owner = request.users.find((user) => user.isOwner) ?? request.users[0]; + const isPickup = !request.team?.name; + const teamName = request.team?.name ?? owner.username; + + const confirmedTime = request.at + ? databaseTimestampToDate(request.at) + : databaseTimestampToDate(postStartTime); + + return ( +
+
+
+ +
+

+ {isPickup ? ( + <> + {t("scrims:pickupBy")} + {owner.username} + + ) : ( + teamName + )} +

+
+ +
+
+ + {request.message ? ( + + ) : null} + + {showFooter ? ( +
+ {canAccept ? ( + + } + data-testid="confirm-modal-trigger-button" + > + {t("scrims:acceptModal.confirmFor", { + time: confirmedTime.toLocaleTimeString(i18n.language, { + hour: "numeric", + minute: "2-digit", + }), + })} + + + ) : ( + + {t("scrims:acceptModal.confirmFor", { + time: confirmedTime.toLocaleTimeString(i18n.language, { + hour: "numeric", + minute: "2-digit", + }), + })} + + } + > + {t("scrims:acceptModal.prevented")} + + )} +
+ ) : null} +
+ ); +} diff --git a/app/features/scrims/components/ScrimFiltersDialog.tsx b/app/features/scrims/components/ScrimFiltersDialog.tsx new file mode 100644 index 000000000..54718eb2b --- /dev/null +++ b/app/features/scrims/components/ScrimFiltersDialog.tsx @@ -0,0 +1,148 @@ +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; +import { useFetcher, useSearchParams } from "@remix-run/react"; +import * as React from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { SendouButton } from "~/components/elements/Button"; +import { SendouDialog } from "~/components/elements/Dialog"; +import { InputFormField } from "~/components/form/InputFormField"; +import { FilterFilledIcon } from "~/components/icons/FilterFilled"; +import { SubmitButton } from "~/components/SubmitButton"; +import { useUser } from "~/features/auth/core/user"; +import type { ScrimFilters } from "~/features/scrims/scrims-types"; +import { scrimsFiltersSchema } from "../scrims-schemas"; +import { LutiDivsFormField } from "./LutiDivsFormField"; + +export function ScrimFiltersDialog({ filters }: { filters: ScrimFilters }) { + const { t } = useTranslation(["scrims"]); + const [isOpen, setIsOpen] = React.useState(false); + + return ( + <> + } + onPress={() => setIsOpen(true)} + data-testid="filter-scrims-button" + > + {t("scrims:filters.button")} + + setIsOpen(false)} + > + { + setIsOpen(false); + }} + /> + + + ); +} + +function FiltersForm({ + filters, + closeDialog, +}: { + filters: ScrimFilters; + closeDialog: () => void; +}) { + const user = useUser(); + const { t } = useTranslation(["scrims"]); + + const methods = useForm({ + resolver: standardSchemaResolver(scrimsFiltersSchema), + defaultValues: filters, + }); + const fetcher = useFetcher(); + const [, setSearchParams] = useSearchParams(); + + const filtersToSearchParams = (newFilters: ScrimFilters) => { + setSearchParams((prev) => { + prev.set("filters", JSON.stringify(newFilters)); + return prev; + }); + }; + + const onApply = React.useCallback( + methods.handleSubmit((values) => { + filtersToSearchParams(values as ScrimFilters); + closeDialog(); + }), + [], + ); + + const onApplyAndPersist = React.useCallback( + methods.handleSubmit((values) => + fetcher.submit( + // @ts-expect-error TODO: fix + { + _action: "PERSIST_SCRIM_FILTERS", + filters: values as Parameters[0], + }, + { + method: "post", + encType: "application/json", + }, + ), + ), + [], + ); + + return ( + + + +
+ + label={t("scrims:filters.weekdayStart")} + name={"weekdayTimes.start" as const} + type="time" + size="extra-small" + /> + + label={t("scrims:filters.weekdayEnd")} + name={"weekdayTimes.end" as const} + type="time" + size="extra-small" + /> +
+ +
+ + label={t("scrims:filters.weekendStart")} + name={"weekendTimes.start" as const} + type="time" + size="extra-small" + /> + + label={t("scrims:filters.weekendEnd")} + name={"weekendTimes.end" as const} + type="time" + size="extra-small" + /> +
+ + + +
+ onApply()}> + {t("scrims:filters.apply")} + + {user ? ( + + {t("scrims:filters.applyAndDefault")} + + ) : null} +
+
+
+ ); +} diff --git a/app/features/scrims/components/ScrimRequestModal.tsx b/app/features/scrims/components/ScrimRequestModal.tsx new file mode 100644 index 000000000..b651fbaea --- /dev/null +++ b/app/features/scrims/components/ScrimRequestModal.tsx @@ -0,0 +1,85 @@ +import { useLoaderData } from "@remix-run/react"; +import { useTranslation } from "react-i18next"; +import { Divider } from "~/components/Divider"; +import { SendouDialog } from "~/components/elements/Dialog"; +import { SelectFormField } from "~/components/form/SelectFormField"; +import { SendouForm } from "~/components/form/SendouForm"; +import { TextAreaFormField } from "~/components/form/TextAreaFormField"; +import { joinListToNaturalString, nullFilledArray } from "~/utils/arrays"; +import { databaseTimestampToDate } from "~/utils/dates"; +import type { loader as scrimsLoader } from "../loaders/scrims.server"; +import type { NewRequestFormFields } from "../routes/scrims"; +import { SCRIM } from "../scrims-constants"; +import { newRequestSchema } from "../scrims-schemas"; +import type { ScrimPost } from "../scrims-types"; +import { generateTimeOptions } from "../scrims-utils"; +import { WithFormField } from "./WithFormField"; + +export function ScrimRequestModal({ + post, + close, +}: { + post: ScrimPost; + close: () => void; +}) { + const { t, i18n } = useTranslation(["scrims"]); + const data = useLoaderData(); + + const timeOptions = post.rangeEnd + ? generateTimeOptions( + databaseTimestampToDate(post.at), + databaseTimestampToDate(post.rangeEnd), + ).map((timestamp) => ({ + value: timestamp, + label: new Date(timestamp).toLocaleTimeString(i18n.language, { + hour: "numeric", + minute: "2-digit", + }), + })) + : []; + + return ( + + 0 + ? { mode: "TEAM", teamId: data.teams[0].id } + : { + mode: "PICKUP", + users: nullFilledArray( + SCRIM.MAX_PICKUP_SIZE_EXCLUDING_OWNER, + ) as unknown as number[], + }, + message: "", + at: post.rangeEnd ? (timeOptions[0]?.value as unknown as Date) : null, + }} + > +
+ {joinListToNaturalString(post.users.map((u) => u.username))} +
+ {post.text ? ( +
{post.text}
+ ) : null} + + + {post.rangeEnd ? ( + + name="at" + label={t("scrims:requestModal.at.label")} + bottomText={t("scrims:requestModal.at.explanation")} + values={timeOptions} + /> + ) : null} + + name="message" + label={t("scrims:requestModal.message.label")} + maxLength={SCRIM.REQUEST_MESSAGE_MAX_LENGTH} + /> +
+
+ ); +} diff --git a/app/features/scrims/core/Scrim.test.ts b/app/features/scrims/core/Scrim.test.ts index 0e28315d3..afdf51f9f 100644 --- a/app/features/scrims/core/Scrim.test.ts +++ b/app/features/scrims/core/Scrim.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; -import type { ScrimPost } from "../scrims-types"; -import { participantIdsListFromAccepted } from "./Scrim"; +import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; +import type { ScrimFilters, ScrimPost } from "../scrims-types"; +import { applyFilters, participantIdsListFromAccepted } from "./Scrim"; type MockUser = { id: number }; type MockRequest = { isAccepted: boolean; users: MockUser[] }; @@ -76,3 +77,362 @@ describe("participantIdsListFromAccepted", () => { expect(result).toEqual([]); }); }); + +describe("applyFilters", () => { + function createPostForFilters( + at: Date, + rangeEnd?: Date, + divs?: { min: string; max: string }, + ): ScrimPost { + return { + id: 1, + at: dateToDatabaseTimestamp(at), + rangeEnd: rangeEnd ? dateToDatabaseTimestamp(rangeEnd) : null, + divs: divs ? { min: divs.min as any, max: divs.max as any } : null, + users: [], + requests: [], + canceled: null, + createdAt: databaseTimestampNow(), + visibility: null, + chatCode: null, + text: "", + maps: null, + isScheduledForFuture: false, + managedByAnyone: false, + mapsTournament: null, + permissions: { MANAGE_REQUESTS: [], CANCEL: [], DELETE_POST: [] }, + team: null, + }; + } + + describe("with no filters", () => { + it("returns true when all filters are null", () => { + const post = createPostForFilters(new Date("2025-01-15T14:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + }); + + describe("division filters", () => { + it("returns true when post has no divs but filter has divs", () => { + const post = createPostForFilters(new Date("2025-01-15T14:00:00")); + const filters: ScrimFilters = { + divs: { min: "5", max: "3" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns true when only filter min is set and post max is at or above filter min", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "6", max: "3" }, + ); + const filters: ScrimFilters = { + divs: { min: "5", max: null }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when only filter min is set and post max is below filter min", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "8", max: "6" }, + ); + const filters: ScrimFilters = { + divs: { min: "5", max: null }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns true when only filter max is set and post min is at or below filter max", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "6", max: "2" }, + ); + const filters: ScrimFilters = { + divs: { min: null, max: "5" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when only filter max is set and post min is above filter max", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "3", max: "1" }, + ); + const filters: ScrimFilters = { + divs: { min: null, max: "5" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns true when post divs overlap with filter divs", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "5", max: "3" }, + ); + const filters: ScrimFilters = { + divs: { min: "6", max: "2" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns true when post divs exactly match filter divs", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "5", max: "3" }, + ); + const filters: ScrimFilters = { + divs: { min: "5", max: "3" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when post divs are too high for filter", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "3", max: "1" }, + ); + const filters: ScrimFilters = { + divs: { min: "6", max: "4" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns false when post divs are too low for filter", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "8", max: "6" }, + ); + const filters: ScrimFilters = { + divs: { min: "5", max: "3" }, + weekdayTimes: null, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + }); + + describe("weekday time filters", () => { + it("returns true when post time overlaps with weekday time filter", () => { + const post = createPostForFilters(new Date("2025-01-15T14:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when post time is before weekday time filter", () => { + const post = createPostForFilters(new Date("2025-01-15T08:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns false when post time is after weekday time filter", () => { + const post = createPostForFilters(new Date("2025-01-15T18:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns true when post time range overlaps with weekday time filter", () => { + const post = createPostForFilters( + new Date("2025-01-15T09:00:00"), + new Date("2025-01-15T11:00:00"), + ); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when post time range does not overlap with weekday time filter", () => { + const post = createPostForFilters( + new Date("2025-01-15T06:00:00"), + new Date("2025-01-15T08:00:00"), + ); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns true when post time range ends exactly at the filter start edge", () => { + const post = createPostForFilters( + new Date("2025-01-15T09:00:00"), + new Date("2025-01-15T10:00:00"), + ); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + }); + + describe("weekend time filters", () => { + it("returns true when post time overlaps with weekend time filter on Saturday", () => { + const post = createPostForFilters(new Date("2025-01-18T14:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: null, + weekendTimes: { start: "10:00", end: "18:00" }, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns true when post time overlaps with weekend time filter on Sunday", () => { + const post = createPostForFilters(new Date("2025-01-19T14:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: null, + weekendTimes: { start: "10:00", end: "18:00" }, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when post time is outside weekend time filter", () => { + const post = createPostForFilters(new Date("2025-01-18T20:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: null, + weekendTimes: { start: "10:00", end: "18:00" }, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("ignores weekday time filter on weekends", () => { + const post = createPostForFilters(new Date("2025-01-18T20:00:00")); + const filters: ScrimFilters = { + divs: null, + weekdayTimes: { start: "10:00", end: "18:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + }); + + describe("combined filters", () => { + it("returns true when both div and time filters match", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "5", max: "3" }, + ); + const filters: ScrimFilters = { + divs: { min: "6", max: "2" }, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(true); + }); + + it("returns false when div filter matches but time filter does not", () => { + const post = createPostForFilters( + new Date("2025-01-15T18:00:00"), + undefined, + { min: "5", max: "3" }, + ); + const filters: ScrimFilters = { + divs: { min: "6", max: "2" }, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns false when time filter matches but div filter does not", () => { + const post = createPostForFilters( + new Date("2025-01-15T14:00:00"), + undefined, + { min: "8", max: "6" }, + ); + const filters: ScrimFilters = { + divs: { min: "5", max: "3" }, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + + it("returns false when neither filter matches", () => { + const post = createPostForFilters( + new Date("2025-01-15T18:00:00"), + undefined, + { min: "8", max: "6" }, + ); + const filters: ScrimFilters = { + divs: { min: "5", max: "3" }, + weekdayTimes: { start: "10:00", end: "16:00" }, + weekendTimes: null, + }; + + expect(applyFilters(post, filters)).toBe(false); + }); + }); +}); diff --git a/app/features/scrims/core/Scrim.ts b/app/features/scrims/core/Scrim.ts index 1b96fd94c..e5ca8f842 100644 --- a/app/features/scrims/core/Scrim.ts +++ b/app/features/scrims/core/Scrim.ts @@ -1,5 +1,9 @@ +import { format, isWeekend } from "date-fns"; +import * as R from "remeda"; +import { databaseTimestampToDate } from "~/utils/dates"; import { logger } from "~/utils/logger"; -import type { ScrimPost } from "../scrims-types"; +import { LUTI_DIVS } from "../scrims-constants"; +import type { ScrimFilters, ScrimPost } from "../scrims-types"; /** Returns true if the original poster has accepted any of the requests. */ export function isAccepted(post: ScrimPost) { @@ -35,3 +39,76 @@ export function participantIdsListFromAccepted(post: ScrimPost) { .map((u) => u.id) .concat(acceptedRequest?.users.map((u) => u.id) ?? []); } + +/** + * Returns the actual start time of the scrim. + * When the post has a time range (rangeEnd is set), returns the accepted request's specific time if available. + * Otherwise returns the post's start time. + */ +export function getStartTime(post: ScrimPost): number { + const acceptedRequest = post.requests.find((r) => r.isAccepted); + return acceptedRequest?.at ?? post.at; +} + +export function applyFilters(post: ScrimPost, filters: ScrimFilters): boolean { + const hasMinFilter = filters.divs?.min !== null; + const hasMaxFilter = filters.divs?.max !== null; + if (filters.divs && (hasMinFilter || hasMaxFilter) && post.divs) { + const postMinIndex = LUTI_DIVS.indexOf(post.divs.min); + const postMaxIndex = LUTI_DIVS.indexOf(post.divs.max); + + if (hasMinFilter && hasMaxFilter) { + const filterMinIndex = LUTI_DIVS.indexOf(filters.divs.min!); + const filterMaxIndex = LUTI_DIVS.indexOf(filters.divs.max!); + + if (postMinIndex < filterMaxIndex || postMaxIndex > filterMinIndex) { + return false; + } + } else if (hasMinFilter) { + const filterMinIndex = LUTI_DIVS.indexOf(filters.divs.min!); + if (postMaxIndex > filterMinIndex) { + return false; + } + } else if (hasMaxFilter) { + const filterMaxIndex = LUTI_DIVS.indexOf(filters.divs.max!); + if (postMinIndex < filterMaxIndex) { + return false; + } + } + } + + const timeFilters = isWeekend(databaseTimestampToDate(post.at)) + ? filters.weekendTimes + : filters.weekdayTimes; + + if (timeFilters) { + const startDate = databaseTimestampToDate(post.at); + const endDate = post.rangeEnd + ? databaseTimestampToDate(post.rangeEnd) + : startDate; + + const startTimeString = format(startDate, "HH:mm"); + const endTimeString = format(endDate, "HH:mm"); + + const hasOverlap = + startTimeString <= timeFilters.end && endTimeString >= timeFilters.start; + + if (!hasOverlap) { + return false; + } + } + + return true; +} + +export function defaultFilters(): ScrimFilters { + return { + weekdayTimes: null, + weekendTimes: null, + divs: null, + }; +} + +export function filtersAreDefault(filters: ScrimFilters): boolean { + return R.isShallowEqual(filters, defaultFilters()); +} diff --git a/app/features/scrims/loaders/scrims.$id.server.ts b/app/features/scrims/loaders/scrims.$id.server.ts index 4e497eb41..e4c61b708 100644 --- a/app/features/scrims/loaders/scrims.$id.server.ts +++ b/app/features/scrims/loaders/scrims.$id.server.ts @@ -1,14 +1,15 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; +import { tournamentDataCached } from "~/features/tournament-bracket/core/Tournament.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { notFoundIfFalsy } from "../../../utils/remix.server"; -import { requireUser } from "../../auth/core/user.server"; +import { + type AuthenticatedUser, + requireUser, +} from "../../auth/core/user.server"; import * as Scrim from "../core/Scrim"; import * as ScrimPostRepository from "../ScrimPostRepository.server"; -import { FF_SCRIMS_ENABLED } from "../scrims-constants"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { - notFoundIfFalsy(FF_SCRIMS_ENABLED); - const user = await requireUser(request); const post = notFoundIfFalsy( @@ -23,10 +24,24 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { throw new Response(null, { status: 403 }); } + const participantIds = Scrim.participantIdsListFromAccepted(post); + return { post, - chatUsers: await UserRepository.findChatUsersByUserIds( - Scrim.participantIdsListFromAccepted(post), - ), + chatUsers: await UserRepository.findChatUsersByUserIds(participantIds), + anyUserPrefersNoScreen: + await UserRepository.anyUserPrefersNoScreen(participantIds), + tournamentMapPool: post.mapsTournament + ? await resolveTournamentMapPool(post.mapsTournament.id, user) + : null, }; }; + +async function resolveTournamentMapPool( + tournamentId: number, + user: AuthenticatedUser, +) { + const data = await tournamentDataCached({ tournamentId, user }); + + return data.ctx.toSetMapPool; +} diff --git a/app/features/scrims/loaders/scrims.new.server.ts b/app/features/scrims/loaders/scrims.new.server.ts index 9c353c552..7a7c196b2 100644 --- a/app/features/scrims/loaders/scrims.new.server.ts +++ b/app/features/scrims/loaders/scrims.new.server.ts @@ -2,15 +2,11 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import * as AssociationRepository from "~/features/associations/AssociationRepository.server"; import { requireUserId } from "~/features/auth/core/user.server"; import type { SerializeFrom } from "~/utils/remix"; -import { notFoundIfFalsy } from "~/utils/remix.server"; import * as TeamRepository from "../../team/TeamRepository.server"; -import { FF_SCRIMS_ENABLED } from "../scrims-constants"; export type ScrimsNewLoaderData = SerializeFrom; export const loader = async ({ request }: LoaderFunctionArgs) => { - notFoundIfFalsy(FF_SCRIMS_ENABLED); - const user = await requireUserId(request); return { diff --git a/app/features/scrims/loaders/scrims.server.ts b/app/features/scrims/loaders/scrims.server.ts index 833c09e1d..85b939e6f 100644 --- a/app/features/scrims/loaders/scrims.server.ts +++ b/app/features/scrims/loaders/scrims.server.ts @@ -2,16 +2,14 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import * as AssociationsRepository from "~/features/associations/AssociationRepository.server"; import * as Association from "~/features/associations/core/Association"; import { getUser } from "~/features/auth/core/user.server"; -import { notFoundIfFalsy } from "~/utils/remix.server"; +import { parseSearchParams } from "~/utils/remix.server"; import * as TeamRepository from "../../team/TeamRepository.server"; import * as Scrim from "../core/Scrim"; import * as ScrimPostRepository from "../ScrimPostRepository.server"; -import { FF_SCRIMS_ENABLED } from "../scrims-constants"; +import { scrimsFiltersSearchParamsObject } from "../scrims-schemas"; import { dividePosts } from "../scrims-utils"; export const loader = async ({ request }: LoaderFunctionArgs) => { - notFoundIfFalsy(FF_SCRIMS_ENABLED); - const user = await getUser(request); const now = new Date(); @@ -19,6 +17,15 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { ? await AssociationsRepository.findByMemberUserId(user?.id) : null; + const filtersFromSearchParams = parseSearchParams({ + request, + schema: scrimsFiltersSearchParamsObject, + }).filters; + + const filters = Scrim.filtersAreDefault(filtersFromSearchParams) + ? user?.preferences?.defaultScrimsFilters + : filtersFromSearchParams; + const posts = (await ScrimPostRepository.findAllRelevant(user?.id)) .filter( (post) => @@ -41,5 +48,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { return { posts: dividePosts(posts, user?.id), teams: user ? await TeamRepository.teamsByMemberUserId(user.id) : [], + filters: filters ?? Scrim.defaultFilters(), }; }; diff --git a/app/features/scrims/routes/scrims.$id.module.css b/app/features/scrims/routes/scrims.$id.module.css index ce37eeb65..46e4ca837 100644 --- a/app/features/scrims/routes/scrims.$id.module.css +++ b/app/features/scrims/routes/scrims.$id.module.css @@ -36,6 +36,7 @@ color: var(--text-lighter); font-size: var(--fonts-xs); line-height: 1.1; + font-weight: var(--semi-bold); } .infoValue { @@ -49,3 +50,46 @@ background-color: var(--bg-lighter-solid); padding: var(--s-2-5); } + +.screenBanIndicator { + display: flex; + align-items: center; + gap: var(--s-1); +} + +.screenBanImageWrapper { + position: relative; + display: inline-block; + line-height: 0; +} + +.screenBanImageWrapper picture { + flex-shrink: 0; +} + +.screenBanIconOverlay { + position: absolute; + bottom: 0; + right: 0; + background-color: var(--bg-lightest-solid); + border-radius: 50%; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.screenBanIconOverlay svg { + width: 14px; + height: 14px; + color: var(--bg); +} + +.screenBanIndicator svg { + color: var(--theme-success); +} + +.screenBanIndicatorWarning svg { + color: var(--theme-warning); +} diff --git a/app/features/scrims/routes/scrims.$id.tsx b/app/features/scrims/routes/scrims.$id.tsx index 270136c61..1e3f4696e 100644 --- a/app/features/scrims/routes/scrims.$id.tsx +++ b/app/features/scrims/routes/scrims.$id.tsx @@ -1,17 +1,25 @@ import { Link, useLoaderData } from "@remix-run/react"; +import clsx from "clsx"; import * as React from "react"; import { useTranslation } from "react-i18next"; import type { z } from "zod/v4"; import { Alert } from "~/components/Alert"; import { SendouButton } from "~/components/elements/Button"; import { SendouDialog } from "~/components/elements/Dialog"; +import { SendouPopover } from "~/components/elements/Popover"; import { SendouForm } from "~/components/form/SendouForm"; import { TextAreaFormField } from "~/components/form/TextAreaFormField"; +import { Image } from "~/components/Image"; +import { AlertIcon } from "~/components/icons/Alert"; +import { CheckmarkIcon } from "~/components/icons/Checkmark"; import TimePopover from "~/components/TimePopover"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { SCRIM } from "~/features/scrims/scrims-constants"; import { cancelScrimSchema } from "~/features/scrims/scrims-schemas"; import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils"; +import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids"; import { useHasPermission } from "~/modules/permissions/hooks"; +import type { SerializeFrom } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { userSubmittedImage } from "~/utils/urls-img"; import { Avatar } from "../../../components/Avatar"; @@ -19,8 +27,10 @@ import { Main } from "../../../components/Main"; import { databaseTimestampToDate } from "../../../utils/dates"; import { logger } from "../../../utils/logger"; import { + mapsPageWithMapPool, navIconUrl, scrimsPage, + specialWeaponImageUrl, teamPage, userPage, } from "../../../utils/urls"; @@ -28,10 +38,9 @@ import { ConnectedChat } from "../../chat/components/Chat"; import { action } from "../actions/scrims.$id.server"; import * as Scrim from "../core/Scrim"; import { loader } from "../loaders/scrims.$id.server"; -import type { ScrimPost as ScrimPostType } from "../scrims-types"; -export { loader, action }; - +import type { ScrimPost, ScrimPost as ScrimPostType } from "../scrims-types"; import styles from "./scrims.$id.module.css"; +export { loader, action }; export const handle: SendouRouteHandle = { i18n: ["scrims", "q"], @@ -96,6 +105,13 @@ export default function ScrimPage() { header={t("q:match.pool")} value={Scrim.resolvePoolCode(data.post.id)} /> + + {data.post.maps || data.tournamentMapPool ? ( + + ) : null}
@@ -127,11 +143,14 @@ function ScrimHeader() { const { t } = useTranslation(["scrims"]); const data = useLoaderData(); + const acceptedRequest = data.post.requests.find((r) => r.isAccepted); + const scrimTime = acceptedRequest?.at ?? data.post.at; + return (

(); + + return ( +
+
{t("scrims:screenBan.header")}
+
+ +
+ {t(`weapons:SPECIAL_${SPLATTERCOLOR_SCREEN_ID}`)} +
+ {data.anyUserPrefersNoScreen ? ( + + ) : ( + + )} +
+
+ + } + > +
+ {data.anyUserPrefersNoScreen + ? t("scrims:screenBan.warning") + : t("scrims:screenBan.allowed")} +
+
+
+
+ ); +} + +function MapsLink({ + maps, + tournamentMapPool, +}: Pick & + Pick, "tournamentMapPool">) { + const { t } = useTranslation(["scrims"]); + + const mapPool = () => { + if (tournamentMapPool) return new MapPool(tournamentMapPool); + + if (maps === "SZ") return MapPool.SZ; + if (maps === "RANKED") return MapPool.ANARCHY; + if (maps === "ALL") return MapPool.ALL; + + logger.info(`Unknown scrim maps value: ${maps}`); + return MapPool.ALL; + }; + + return ( +
+
{t("scrims:maps.header")}
+ + Generate maplist + +
+ ); +} + function ScrimChat() { const data = useLoaderData(); diff --git a/app/features/scrims/routes/scrims.module.css b/app/features/scrims/routes/scrims.module.css index 23bb32b1e..58faf7880 100644 --- a/app/features/scrims/routes/scrims.module.css +++ b/app/features/scrims/routes/scrims.module.css @@ -9,6 +9,8 @@ padding: var(--s-0-5) var(--s-2); font-weight: var(--bold); width: max-content; + display: flex; + gap: var(--s-0-5); } .postPrivateCell { @@ -65,3 +67,19 @@ position: sticky; right: 0; } + +.cardsGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: var(--s-4); +} + +.filterButtons { + display: flex; + gap: var(--s-6); + align-items: center; +} + +.filterButtons button:not(.active) { + color: var(--text-lighter); +} diff --git a/app/features/scrims/routes/scrims.new.test.ts b/app/features/scrims/routes/scrims.new.test.ts index bd972a243..d00b10e11 100644 --- a/app/features/scrims/routes/scrims.new.test.ts +++ b/app/features/scrims/routes/scrims.new.test.ts @@ -33,6 +33,8 @@ const defaultNewScrimPostArgs: Parameters[0] = { notFoundVisibility: { forAssociation: "PUBLIC", }, + maps: "NO_PREFERENCE", + mapsTournamentId: null, }; describe("New scrim post action", () => { diff --git a/app/features/scrims/routes/scrims.new.tsx b/app/features/scrims/routes/scrims.new.tsx index 23b6760e4..4d07b25c6 100644 --- a/app/features/scrims/routes/scrims.new.tsx +++ b/app/features/scrims/routes/scrims.new.tsx @@ -3,7 +3,9 @@ import * as React from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import type { z } from "zod/v4"; +import { TournamentSearch } from "~/components/elements/TournamentSearch"; import { DateFormField } from "~/components/form/DateFormField"; +import { SelectFormField } from "~/components/form/SelectFormField"; import { SendouForm } from "~/components/form/SendouForm"; import { TextAreaFormField } from "~/components/form/TextAreaFormField"; import { ToggleFormField } from "~/components/form/ToggleFormField"; @@ -13,14 +15,14 @@ import type { SendouRouteHandle } from "~/utils/remix.server"; import { FormMessage } from "../../../components/FormMessage"; import { Main } from "../../../components/Main"; import { action } from "../actions/scrims.new.server"; +import { LutiDivsFormField } from "../components/LutiDivsFormField"; import { WithFormField } from "../components/WithFormField"; import { loader, type ScrimsNewLoaderData } from "../loaders/scrims.new.server"; -import { LUTI_DIVS, SCRIM } from "../scrims-constants"; +import { SCRIM } from "../scrims-constants"; import { MAX_SCRIM_POST_TEXT_LENGTH, scrimsNewActionSchema, } from "../scrims-schemas"; -import type { LutiDiv } from "../scrims-types"; export { loader, action }; export const handle: SendouRouteHandle = { @@ -46,6 +48,7 @@ export default function NewScrimPage() { defaultValues={{ postText: "", at: new Date(), + rangeEnd: null, divs: null, baseVisibility: "PUBLIC", notFoundVisibility: DEFAULT_NOT_FOUND_VISIBILITY, @@ -59,6 +62,8 @@ export default function NewScrimPage() { ) as unknown as number[], }, managedByAnyone: true, + maps: "NO_PREFERENCE", + mapsTournamentId: null, }} > @@ -69,6 +74,12 @@ export default function NewScrimPage() { bottomText={t("scrims:forms.when.explanation")} granularity="minute" /> + + label={t("scrims:forms.rangeEnd.title")} + name="rangeEnd" + bottomText={t("scrims:forms.rangeEnd.explanation")} + granularity="minute" + /> @@ -76,6 +87,23 @@ export default function NewScrimPage() { + + label={t("scrims:forms.maps.title")} + name="maps" + values={[ + { + value: "NO_PREFERENCE", + label: t("scrims:forms.maps.noPreference"), + }, + { value: "SZ", label: t("scrims:forms.maps.szOnly") }, + { value: "RANKED", label: t("scrims:forms.maps.rankedOnly") }, + { value: "ALL", label: t("scrims:forms.maps.allModes") }, + { value: "TOURNAMENT", label: t("scrims:forms.maps.tournament") }, + ]} + /> + + + label={t("scrims:forms.text.title")} name="postText" @@ -209,89 +237,38 @@ const AssociationSelect = React.forwardRef< ); }); -function LutiDivsFormField() { +function TournamentSearchFormField() { + const { t } = useTranslation(["scrims"]); const methods = useFormContext(); + const maps = useWatch({ name: "maps" }); - const error = methods.formState.errors.divs; + const error = methods.formState.errors.mapsTournamentId; + + React.useEffect(() => { + if (maps !== "TOURNAMENT") { + methods.setValue("mapsTournamentId", null); + } + }, [maps, methods]); + + if (maps !== "TOURNAMENT") return null; return (
( - + name="mapsTournamentId" + render={({ field: { onChange, value } }) => ( + onChange(tournament.id)} + /> )} /> - {error && ( + {error ? ( {error.message as string} - )} -
- ); -} - -type LutiDivEdit = { - max: LutiDiv | null; - min: LutiDiv | null; -}; - -function LutiDivsSelector({ - value, - onChange, - onBlur, -}: { - value: LutiDivEdit | null; - onChange: (value: LutiDivEdit | null) => void; - onBlur: () => void; -}) { - const { t } = useTranslation(["scrims"]); - - const onChangeMin = (e: React.ChangeEvent) => { - const newValue = e.target.value === "" ? null : (e.target.value as LutiDiv); - - onChange( - newValue || value?.max - ? { min: newValue, max: value?.max ?? null } - : null, - ); - }; - - const onChangeMax = (e: React.ChangeEvent) => { - const newValue = e.target.value === "" ? null : (e.target.value as LutiDiv); - - onChange( - newValue || value?.min - ? { max: newValue, min: value?.min ?? null } - : null, - ); - }; - - return ( -
-
- - -
- -
- - -
+ ) : null}
); } diff --git a/app/features/scrims/routes/scrims.tsx b/app/features/scrims/routes/scrims.tsx index 82c4719f0..da7fb0a48 100644 --- a/app/features/scrims/routes/scrims.tsx +++ b/app/features/scrims/routes/scrims.tsx @@ -1,40 +1,23 @@ import type { MetaFunction } from "@remix-run/node"; -import { Link, useLoaderData } from "@remix-run/react"; +import { useLoaderData } from "@remix-run/react"; import clsx from "clsx"; -import { formatDistance } from "date-fns"; import * as React from "react"; import { useTranslation } from "react-i18next"; import * as R from "remeda"; import type { z } from "zod/v4"; import { AddNewButton } from "~/components/AddNewButton"; -import { Avatar } from "~/components/Avatar"; -import { Divider } from "~/components/Divider"; import { LinkButton, SendouButton } from "~/components/elements/Button"; -import { SendouDialog } from "~/components/elements/Dialog"; -import { SendouPopover } from "~/components/elements/Popover"; -import { FormWithConfirm } from "~/components/FormWithConfirm"; -import { SendouForm } from "~/components/form/SendouForm"; -import { EyeSlashIcon } from "~/components/icons/EyeSlash"; -import { SpeechBubbleIcon } from "~/components/icons/SpeechBubble"; -import { UsersIcon } from "~/components/icons/Users"; -import { Table } from "~/components/Table"; -import TimePopover from "~/components/TimePopover"; import { useUser } from "~/features/auth/core/user"; import { useIsMounted } from "~/hooks/useIsMounted"; -import { joinListToNaturalString, nullFilledArray } from "~/utils/arrays"; import { databaseTimestampToDate } from "~/utils/dates"; -import invariant from "~/utils/invariant"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { associationsPage, navIconUrl, newScrimPostPage, - scrimPage, scrimsPage, - userPage, } from "~/utils/urls"; -import { userSubmittedImage } from "~/utils/urls-img"; import { SendouTab, SendouTabList, @@ -42,20 +25,18 @@ import { SendouTabs, } from "../../../components/elements/Tabs"; import { ArrowDownOnSquareIcon } from "../../../components/icons/ArrowDownOnSquare"; -import { ArrowUpOnSquareIcon } from "../../../components/icons/ArrowUpOnSquare"; import { CheckmarkIcon } from "../../../components/icons/Checkmark"; -import { ClockIcon } from "../../../components/icons/Clock"; -import { CrossIcon } from "../../../components/icons/Cross"; +import { FilterIcon } from "../../../components/icons/Filter"; import { MegaphoneIcon } from "../../../components/icons/MegaphoneIcon"; -import { SpeechBubbleFilledIcon } from "../../../components/icons/SpeechBubbleFilled"; import { Main } from "../../../components/Main"; import { action } from "../actions/scrims.server"; -import { WithFormField } from "../components/WithFormField"; +import { ScrimPostCard, ScrimRequestCard } from "../components/ScrimCard"; +import { ScrimFiltersDialog } from "../components/ScrimFiltersDialog"; +import * as Scrim from "../core/Scrim"; import { loader } from "../loaders/scrims.server"; -import { SCRIM } from "../scrims-constants"; -import { newRequestSchema } from "../scrims-schemas"; -import type { ScrimPost, ScrimPostRequest } from "../scrims-types"; -export { loader, action }; +import type { newRequestSchema } from "../scrims-schemas"; +import type { ScrimFilters, ScrimPost } from "../scrims-types"; +export { action, loader }; import styles from "./scrims.module.css"; @@ -85,12 +66,6 @@ export default function ScrimsPage() { const { t } = useTranslation(["calendar", "scrims"]); const data = useLoaderData(); const isMounted = useIsMounted(); - const [scrimToRequestId, setScrimToRequestId] = React.useState(); - - // biome-ignore lint/correctness/useExhaustiveDependencies: clear modal on submit - React.useEffect(() => { - setScrimToRequestId(undefined); - }, [data]); if (!isMounted) return ( @@ -102,72 +77,67 @@ export default function ScrimsPage() { return (
- - {t("scrims:associations.title")} - +
+ + {t("scrims:associations.title")} + + {user ? ( + + ) : null} +
- {typeof scrimToRequestId === "number" ? ( - setScrimToRequestId(undefined)} - /> - ) : null} 0 ? "owned" : "available"} + defaultSelectedKey={ + data.posts.owned.length > 0 + ? "owned" + : data.posts.booked.length > 0 + ? "booked" + : "available" + } > - - } - number={data.posts.owned.length} - > - {t("scrims:tabs.owned")} - - } - number={data.posts.requested.length} - data-testid="requests-scrims-tab" - > - {t("scrims:tabs.requests")} - - } - number={data.posts.neutral.length} - data-testid="available-scrims-tab" - > - {t("scrims:tabs.available")} - - - - - - - - + {user ? ( + + } + number={data.posts.neutral.length} + data-testid="available-scrims-tab" + > + {t("scrims:tabs.available")} + + } + number={data.posts.owned.length} + > + {t("scrims:tabs.owned")} + + } + number={data.posts.booked.length} + data-testid="booked-scrims-tab" + > + {t("scrims:tabs.booked")} + + + ) : null} {data.posts.neutral.length > 0 ? ( - ) : (
@@ -175,6 +145,24 @@ export default function ScrimsPage() {
)}
+ + {data.posts.owned.length > 0 ? ( + + ) : ( +
+ {t("scrims:noOwnedPosts")} +
+ )} +
+ + {data.posts.booked.length > 0 ? ( + + ) : ( +
+ {t("scrims:noBookedScrims")} +
+ )} +
{t("calendar:inYourTimeZone")}{" "} @@ -184,70 +172,240 @@ export default function ScrimsPage() { ); } -function RequestScrimModal({ - postId, - close, +function ScrimsDaySeparatedCards({ + posts, + filters, }: { - postId: number; - close: () => void; + posts: ScrimPost[]; + filters: ScrimFilters; }) { - const { t } = useTranslation(["scrims"]); - const data = useLoaderData(); - - // both to avoid crash when requesting - const post = [...data.posts.neutral, ...data.posts.requested].find( - (post) => post.id === postId, + const postsByDay = R.groupBy(posts, (post) => + databaseTimestampToDate(post.at).getDate(), ); - invariant(post, "Post not found"); return ( - - 0 - ? { mode: "TEAM", teamId: data.teams[0].id } - : { - mode: "PICKUP", - users: nullFilledArray( - SCRIM.MAX_PICKUP_SIZE_EXCLUDING_OWNER, - ) as unknown as number[], - }, - }} - > - -
- {joinListToNaturalString(post.users.map((u) => u.username))} -
- {post.text ? ( -
{post.text}
- ) : null} - - -
-
+
+ {Object.entries(postsByDay) + .sort((a, b) => a[1][0].at - b[1][0].at) + .map(([day, dayPosts]) => ( + + ))} +
); } -function ScrimsDaySeparatedTables({ +function ScrimsDaySection({ posts, - showPopovers = true, - showDeletePost = false, - showRequestRows = false, - showStatus = false, - requestScrim, + filters, }: { posts: ScrimPost[]; - showPopovers?: boolean; - showDeletePost?: boolean; - showRequestRows?: boolean; - showStatus?: boolean; - requestScrim?: (postId: number) => void; + filters: ScrimFilters; }) { const { i18n } = useTranslation(); + const user = useUser(); + const [showFiltered, setShowFiltered] = React.useState(false); + const [showRequestPending, setShowRequestPending] = React.useState(false); + + const filteredPosts = posts.filter((post) => + Scrim.applyFilters(post, filters), + ); + + const pendingRequestsCount = filteredPosts.filter((post) => + post.requests.some((request) => + request.users.some((rUser) => user?.id === rUser.id), + ), + ).length; + + return ( +
+
+

+ {databaseTimestampToDate(posts[0].at).toLocaleDateString( + i18n.language, + { + day: "numeric", + month: "long", + weekday: "long", + }, + )} +

+ {user ? ( + + ) : null} +
+
+ {(showFiltered ? posts : filteredPosts).map((post) => { + const hasRequested = post.requests.some((request) => + request.users.some((rUser) => user?.id === rUser.id), + ); + + if (hasRequested && !showRequestPending) { + return null; + } + + const getAction = () => { + if (!user) return undefined; + if (hasRequested) return "VIEW_REQUEST"; + if (post.requests.length === 0) return "REQUEST"; + return undefined; + }; + + const isFilteredOut = + showFiltered && !Scrim.applyFilters(post, filters); + + return ( + + ); + })} +
+
+ ); +} + +function AvailableScrimsFilterButtons({ + showFiltered, + setShowFiltered, + showRequestPending, + setShowRequestPending, + pendingRequestsCount, + filteredCount, +}: { + showFiltered: boolean; + setShowFiltered: (value: boolean) => void; + showRequestPending: boolean; + setShowRequestPending: (value: boolean) => void; + pendingRequestsCount: number; + filteredCount: number; +}) { + const { t } = useTranslation(["scrims"]); + + if (filteredCount === 0 && pendingRequestsCount === 0) { + return null; + } + + return ( +
+ {filteredCount > 0 ? ( + setShowFiltered(!showFiltered)} + icon={} + className={showFiltered ? styles.active : undefined} + > + {showFiltered + ? t("scrims:filters.hideFiltered", { count: filteredCount }) + : t("scrims:filters.showFiltered", { count: filteredCount })} + + ) : null} + {pendingRequestsCount > 0 ? ( + setShowRequestPending(!showRequestPending)} + icon={} + className={showRequestPending ? styles.active : undefined} + > + {showRequestPending + ? t("scrims:filters.hidePendingRequests", { + count: pendingRequestsCount, + }) + : t("scrims:filters.showPendingRequests", { + count: pendingRequestsCount, + })} + + ) : null} +
+ ); +} + +function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) { + const { i18n, t } = useTranslation(["scrims"]); + const user = useUser(); + + const postsByDay = R.groupBy(posts, (post) => + databaseTimestampToDate(post.at).getDate(), + ); + + return ( +
+ {Object.entries(postsByDay) + .sort((a, b) => a[1][0].at - b[1][0].at) + .map(([day, posts]) => { + return ( +
+

+ {databaseTimestampToDate(posts![0].at).toLocaleDateString( + i18n.language, + { + day: "numeric", + month: "long", + weekday: "long", + }, + )} +

+
+ {posts!.map((post) => { + const isAccepted = post.requests.some( + (request) => request.isAccepted, + ); + const canDelete = + user && + post.permissions.DELETE_POST.includes(user.id) && + !isAccepted; + + return ( +
+ + {post.requests.length > 0 ? ( +
+ {post.requests.map((request) => ( + + ))} +
+ ) : ( +
+ {t("scrims:noRequestsYet")} +
+ )} +
+ ); + })} +
+
+ ); + })} +
+ ); +} + +function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) { + const { i18n } = useTranslation(); const postsByDay = R.groupBy(posts, (post) => databaseTimestampToDate(post.at).getDate(), @@ -270,434 +428,30 @@ function ScrimsDaySeparatedTables({ }, )}

- +
+ {posts!.map((post) => { + const acceptedRequest = post.requests.find( + (request) => request.isAccepted, + ); + + return ( +
+ + {acceptedRequest ? ( + + ) : null} +
+ ); + })} +
); })} ); } - -function ScrimsTable({ - posts, - showPopovers, - showDeletePost, - showRequestRows, - showStatus, - requestScrim, -}: { - posts: ScrimPost[]; - showPopovers: boolean; - showDeletePost: boolean; - showRequestRows: boolean; - showStatus: boolean; - requestScrim?: (postId: number) => void; -}) { - const { t } = useTranslation(["common", "scrims"]); - const user = useUser(); - - invariant( - !(requestScrim && showDeletePost), - "Can't have both request scrim and delete post", - ); - - const getStatus = (post: ScrimPost) => { - if (post.canceled) return "CANCELED"; - if (post.requests.at(0)?.isAccepted) return "CONFIRMED"; - if ( - post.requests.some((r) => r.users.some((rUser) => user?.id === rUser.id)) - ) { - return "PENDING"; - } - - return null; - }; - - return ( - - - - - - {showPopovers ? - {showStatus ? : null} - {requestScrim || showDeletePost ? - - - {posts.map((post) => { - const owner = - post.users.find((user) => user.isOwner) ?? post.users[0]; - - const requests = showRequestRows - ? post.requests.map((request) => ( - - )) - : []; - - const isAccepted = post.requests.some( - (request) => request.isAccepted, - ); - - const showContactButton = - isAccepted && - post.requests.at(0)?.users.some((rUser) => rUser.id === user?.id); - - const status = getStatus(post); - - return ( - - - - - {showPopovers ? ( - - ) : null} - - {showStatus ? ( - - ) : null} - {user && requestScrim && post.requests.length === 0 ? ( - - ) : null} - {showDeletePost && !isAccepted ? ( - - ) : null} - {user && - requestScrim && - post.requests.length !== 0 && - !post.requests.at(0)?.isAccepted && - post.requests.at(0)?.permissions.CANCEL.includes(user.id) ? ( - - ) : null} - {showContactButton ? ( - - ) : null} - {isAccepted && - post.requests.some( - (r) => - r.isAccepted && !r.users.some((u) => u.id === user?.id), - ) ? ( - - {requests} - - ); - })} - -
{t("scrims:table.headers.time")}{t("scrims:table.headers.team")} : null} - {t("scrims:table.headers.divs")}{t("scrims:table.headers.status")} : null} -
-
-
- {!post.isScheduledForFuture ? ( - t("scrims:now") - ) : ( - - )} -
- {post.isPrivate ? ( - } - data-testid="limited-visibility-popover" - /> - } - > - {t("scrims:limitedVisibility")} - - ) : null} -
-
-
- {showPopovers ? ( - } - /> - } - > -
- {post.users.map((user) => ( - - - {user.username} - - ))} -
-
- ) : null} - {post.team?.avatarUrl ? ( - - ) : ( - - )} - {post.team?.name ?? - t("scrims:pickup", { username: owner.username })} -
-
- {post.text ? ( - - } - data-testid="scrim-text-popover" - /> - } - > - {post.text} - - ) : null} - - {post.divs ? ( - <> - {post.divs.max} - {post.divs.min} - - ) : null} - -
- {status === "CONFIRMED" ? ( - <> - {t("scrims:status.booked")} - - ) : null} - {status === "PENDING" ? ( - <> - {t("scrims:status.pending")} - - ) : null} - {status === "CANCELED" ? ( - <> - {t("scrims:status.canceled")} - - ) : null} -
-
- requestScrim(post.id)} - icon={} - className="ml-auto" - > - {t("scrims:actions.request")} - - - {user && post.permissions.DELETE_POST.includes(user.id) ? ( - - - {t("common:actions.delete")} - - - ) : ( - - {t("common:actions.delete")} - - } - > - {t("scrims:deleteModal.prevented", { - username: owner.username, - })} - - )} - - - } - className="ml-auto" - > - {t("common:actions.cancel")} - - - - - - ) : null} -
- ); -} - -function ContactButton({ postId }: { postId: number }) { - const { t } = useTranslation(["scrims"]); - - return ( - } - > - {t("scrims:actions.contact")} - - ); -} - -function RequestRow({ - canAccept, - request, - postId, -}: { - canAccept: boolean; - request: ScrimPostRequest; - postId: number; -}) { - const { t } = useTranslation(["common", "scrims"]); - - const requestOwner = - request.users.find((user) => user.isOwner) ?? request.users[0]; - - const groupName = - request.team?.name ?? - t("scrims:pickup", { - username: requestOwner.username, - }); - - return ( - - - -
- } - variant="minimal" - /> - } - > -
- {request.users.map((user) => ( - - - {user.username} - - ))} -
-
- {request.team?.avatarUrl ? ( - - ) : ( - - )} - {groupName} -
- - - - - - {!request.isAccepted && canAccept ? ( - - - {t("common:actions.accept")} - - - ) : !request.isAccepted && !canAccept ? ( - - {t("common:actions.accept")} - - } - > - {t("scrims:acceptModal.prevented")} - - ) : ( - - )} - - - ); -} diff --git a/app/features/scrims/scrims-constants.ts b/app/features/scrims/scrims-constants.ts index e5057f966..24eff7941 100644 --- a/app/features/scrims/scrims-constants.ts +++ b/app/features/scrims/scrims-constants.ts @@ -17,6 +17,6 @@ export const SCRIM = { MAX_PICKUP_SIZE_EXCLUDING_OWNER: 5, MIN_MEMBERS_PER_TEAM: 4, CANCEL_REASON_MAX_LENGTH: 500, + REQUEST_MESSAGE_MAX_LENGTH: 200, + MAX_TIME_RANGE_MS: 3 * 60 * 60 * 1000, // 3 hours }; - -export const FF_SCRIMS_ENABLED = true; diff --git a/app/features/scrims/scrims-schemas.test.ts b/app/features/scrims/scrims-schemas.test.ts new file mode 100644 index 000000000..f67278709 --- /dev/null +++ b/app/features/scrims/scrims-schemas.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { divsSchema } from "./scrims-schemas"; + +describe("divsSchema", () => { + it("swaps min and max when max is lower skill than min", () => { + const result = divsSchema.parse({ min: "1", max: "10" }); + + expect(result).toEqual({ min: "10", max: "1" }); + }); + + it("keeps min and max when they are in correct order", () => { + const result = divsSchema.parse({ min: "10", max: "1" }); + + expect(result).toEqual({ min: "10", max: "1" }); + }); + + it("keeps min and max when they are equal", () => { + const result = divsSchema.parse({ min: "5", max: "5" }); + + expect(result).toEqual({ min: "5", max: "5" }); + }); +}); diff --git a/app/features/scrims/scrims-schemas.ts b/app/features/scrims/scrims-schemas.ts index ac54ce480..e7192a161 100644 --- a/app/features/scrims/scrims-schemas.ts +++ b/app/features/scrims/scrims-schemas.ts @@ -7,6 +7,8 @@ import { filterOutNullishMembers, id, noDuplicates, + safeJSONParse, + timeString, } from "~/utils/zod"; import { associationIdentifierSchema } from "../associations/associations-schemas"; import { LUTI_DIVS, SCRIM } from "./scrims-constants"; @@ -38,6 +40,11 @@ export const newRequestSchema = z.object({ _action: _action("NEW_REQUEST"), scrimPostId: id, from: fromSchema, + message: z.preprocess( + falsyToNull, + z.string().max(SCRIM.REQUEST_MESSAGE_MAX_LENGTH).nullable(), + ), + at: z.preprocess(date, z.date()).nullish(), }); export const acceptRequestSchema = z.object({ @@ -54,11 +61,65 @@ export const cancelScrimSchema = z.object({ reason: z.string().trim().min(1).max(SCRIM.CANCEL_REASON_MAX_LENGTH), }); +const timeRangeSchema = z.object({ + start: timeString, + end: timeString, +}); + +export const divsSchema = z + .object({ + min: z.enum(LUTI_DIVS).nullable(), + max: z.enum(LUTI_DIVS).nullable(), + }) + .refine( + (div) => { + if (!div) return true; + + if (div.max && !div.min) return false; + if (div.min && !div.max) return false; + + return true; + }, + { + message: "Both min and max div must be set or neither", + }, + ) + .transform((divs) => { + if (!divs.min || !divs.max) return divs; + + const minIndex = LUTI_DIVS.indexOf(divs.min); + const maxIndex = LUTI_DIVS.indexOf(divs.max); + + if (maxIndex > minIndex) { + return { min: divs.max, max: divs.min }; + } + + return divs; + }); + +export const scrimsFiltersSchema = z.object({ + weekdayTimes: timeRangeSchema.nullable().catch(null), + weekendTimes: timeRangeSchema.nullable().catch(null), + divs: divsSchema.nullable().catch(null), +}); + +export const scrimsFiltersSearchParamsObject = z.object({ + filters: z + .preprocess(safeJSONParse, scrimsFiltersSchema) + .catch({ weekdayTimes: null, weekendTimes: null, divs: null }), +}); + +export const persistScrimFiltersSchema = z.object({ + _action: _action("PERSIST_SCRIM_FILTERS"), + filters: scrimsFiltersSchema, +}); + export const scrimsActionSchema = z.union([ deletePostSchema, newRequestSchema, acceptRequestSchema, cancelRequestSchema, + persistScrimFiltersSchema, ]); export const MAX_SCRIM_POST_TEXT_LENGTH = 500; @@ -90,6 +151,33 @@ export const scrimsNewActionSchema = z }, ), ), + rangeEnd: z + .preprocess(date, z.date()) + .nullish() + .refine( + (date) => { + if (!date) return true; + + if (date < sub(new Date(), { days: 1 })) return false; + + return true; + }, + { + message: "Date can not be in the past", + }, + ) + .refine( + (date) => { + if (!date) return true; + + if (date > add(new Date(), { days: 15 })) return false; + + return true; + }, + { + message: "Date can not be more than 2 weeks in the future", + }, + ), baseVisibility: associationIdentifierSchema, notFoundVisibility: z.object({ at: z @@ -109,44 +197,32 @@ export const scrimsNewActionSchema = z ), forAssociation: associationIdentifierSchema, }), - divs: z - .object({ - min: z.enum(LUTI_DIVS).nullable(), - max: z.enum(LUTI_DIVS).nullable(), - }) - .nullable() - .refine( - (div) => { - if (!div) return true; - - if (div.max && !div.min) return false; - if (div.min && !div.max) return false; - - return true; - }, - { - message: "Both min and max div must be set or neither", - }, - ) - .refine( - (divs) => { - if (!divs?.min || !divs.max) return true; - - const minIndex = LUTI_DIVS.indexOf(divs.min); - const maxIndex = LUTI_DIVS.indexOf(divs.max); - - return minIndex >= maxIndex; - }, - { message: "Min div must be less than or equal to max div" }, - ), + divs: divsSchema.nullable(), from: fromSchema, postText: z.preprocess( falsyToNull, z.string().max(MAX_SCRIM_POST_TEXT_LENGTH).nullable(), ), managedByAnyone: z.boolean(), + maps: z.enum(["NO_PREFERENCE", "SZ", "RANKED", "ALL", "TOURNAMENT"]), + mapsTournamentId: z.preprocess(falsyToNull, id.nullable()), }) .superRefine((post, ctx) => { + if (post.maps === "TOURNAMENT" && !post.mapsTournamentId) { + ctx.addIssue({ + path: ["mapsTournamentId"], + message: "Tournament must be selected when maps is tournament", + code: z.ZodIssueCode.custom, + }); + } + + if (post.maps !== "TOURNAMENT" && post.mapsTournamentId) { + ctx.addIssue({ + path: ["mapsTournamentId"], + message: "Tournament should only be selected when maps is tournament", + code: z.ZodIssueCode.custom, + }); + } if ( post.notFoundVisibility.at && post.notFoundVisibility.forAssociation === post.baseVisibility @@ -182,4 +258,24 @@ export const scrimsNewActionSchema = z code: z.ZodIssueCode.custom, }); } + + if (post.rangeEnd && post.rangeEnd <= post.at) { + ctx.addIssue({ + path: ["rangeEnd"], + message: "End time must be after start time", + code: z.ZodIssueCode.custom, + }); + } + + if ( + post.rangeEnd && + post.rangeEnd.getTime() - post.at.getTime() > SCRIM.MAX_TIME_RANGE_MS + ) { + ctx.addIssue({ + path: ["rangeEnd"], + message: + "Time range can not be more than 3 hours. Make separate posts instead", + code: z.ZodIssueCode.custom, + }); + } }); diff --git a/app/features/scrims/scrims-types.ts b/app/features/scrims/scrims-types.ts index 9e2ac6d24..002d08fd2 100644 --- a/app/features/scrims/scrims-types.ts +++ b/app/features/scrims/scrims-types.ts @@ -7,6 +7,7 @@ export type LutiDiv = (typeof LUTI_DIVS)[number]; export interface ScrimPost { id: number; at: number; + rangeEnd: number | null; createdAt: number; visibility: AssociationVisibility | null; text: string | null; @@ -16,6 +17,12 @@ export interface ScrimPost { /** Min div in the whole system is "11" */ min: LutiDiv; } | null; + maps: "SZ" | "ALL" | "RANKED" | null; + mapsTournament: { + id: number; + name: string; + avatarUrl: string; + } | null; team: ScrimPostTeam | null; users: Array; chatCode: string | null; @@ -42,6 +49,8 @@ export interface ScrimPostRequest { isAccepted: boolean; users: Array; team: ScrimPostTeam | null; + message: string | null; + at: number | null; permissions: { CANCEL: number[]; }; @@ -57,3 +66,17 @@ interface ScrimPostTeam { customUrl: string; avatarUrl: string | null; } + +export interface TimeRange { + start: string; + end: string; +} + +export interface ScrimFilters { + weekdayTimes: TimeRange | null; + weekendTimes: TimeRange | null; + divs: { + min: LutiDiv | null; + max: LutiDiv | null; + } | null; +} diff --git a/app/features/scrims/scrims-utils.test.ts b/app/features/scrims/scrims-utils.test.ts new file mode 100644 index 000000000..18d6e306c --- /dev/null +++ b/app/features/scrims/scrims-utils.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, it } from "vitest"; +import { formatFlexTimeDisplay, generateTimeOptions } from "./scrims-utils"; + +describe("generateTimeOptions", () => { + it("includes both start and end times", () => { + const start = new Date("2025-01-15T14:15:00"); + const end = new Date("2025-01-15T16:45:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toContain(start.getTime()); + expect(result).toContain(end.getTime()); + }); + + it("includes all :00 and :30 times in range", () => { + const start = new Date("2025-01-15T14:00:00"); + const end = new Date("2025-01-15T16:00:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toContain(new Date("2025-01-15T14:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T14:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T16:00:00").getTime()); + }); + + it("clears seconds and milliseconds from all times", () => { + const start = new Date("2025-01-15T14:15:23.456"); + const end = new Date("2025-01-15T15:45:59.999"); + + const result = generateTimeOptions(start, end); + + for (const timestamp of result) { + const date = new Date(timestamp); + expect(date.getSeconds()).toBe(0); + expect(date.getMilliseconds()).toBe(0); + } + }); + + it("returns sorted timestamps", () => { + const start = new Date("2025-01-15T14:15:00"); + const end = new Date("2025-01-15T16:45:00"); + + const result = generateTimeOptions(start, end); + + for (let i = 1; i < result.length; i++) { + expect(result[i]).toBeGreaterThan(result[i - 1]); + } + }); + + it("handles start time between :00 and :30", () => { + const start = new Date("2025-01-15T14:10:00"); + const end = new Date("2025-01-15T15:00:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toContain(new Date("2025-01-15T14:10:00").getTime()); + expect(result).toContain(new Date("2025-01-15T14:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:00:00").getTime()); + }); + + it("handles start time between :30 and :00", () => { + const start = new Date("2025-01-15T14:45:00"); + const end = new Date("2025-01-15T16:00:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toContain(new Date("2025-01-15T14:45:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T16:00:00").getTime()); + }); + + it("handles range less than 30 minutes", () => { + const start = new Date("2025-01-15T14:15:00"); + const end = new Date("2025-01-15T14:25:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toEqual([ + new Date("2025-01-15T14:15:00").getTime(), + new Date("2025-01-15T14:25:00").getTime(), + ]); + }); + + it("handles exact hour boundaries", () => { + const start = new Date("2025-01-15T14:00:00"); + const end = new Date("2025-01-15T17:00:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toContain(new Date("2025-01-15T14:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T14:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T16:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T16:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T17:00:00").getTime()); + }); + + it("handles exact half-hour boundaries", () => { + const start = new Date("2025-01-15T14:30:00"); + const end = new Date("2025-01-15T16:30:00"); + + const result = generateTimeOptions(start, end); + + expect(result).toContain(new Date("2025-01-15T14:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T15:30:00").getTime()); + expect(result).toContain(new Date("2025-01-15T16:00:00").getTime()); + expect(result).toContain(new Date("2025-01-15T16:30:00").getTime()); + }); + + it("does not include duplicate times", () => { + const start = new Date("2025-01-15T14:00:00"); + const end = new Date("2025-01-15T15:00:00"); + + const result = generateTimeOptions(start, end); + + const uniqueValues = new Set(result); + expect(result.length).toBe(uniqueValues.size); + }); + + it("handles maximum 3-hour range", () => { + const start = new Date("2025-01-15T14:00:00"); + const end = new Date("2025-01-15T17:00:00"); + + const result = generateTimeOptions(start, end); + + expect(result.length).toBe(7); + }); +}); + +describe("formatFlexTimeDisplay", () => { + it("returns null when totalMinutes is 0", () => { + const timestamp = Math.floor( + new Date("2025-01-15T14:00:00").getTime() / 1000, + ); + + const result = formatFlexTimeDisplay(timestamp, timestamp); + + expect(result).toBeNull(); + }); + + it("returns null when endTimestamp is before startTimestamp", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T13:00:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBeNull(); + }); + + it("returns formatted minutes when only minutes (no hours)", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T14:45:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+45m"); + }); + + it("returns formatted hours when exactly on the hour", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T16:00:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+2h"); + }); + + it("returns formatted hours and minutes when both present", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T15:30:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+1h 30m"); + }); + + it("handles 1 minute difference", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T14:01:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+1m"); + }); + + it("handles 1 hour difference", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T15:00:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+1h"); + }); + + it("handles multiple hours and minutes", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T17:25:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+3h 25m"); + }); + + it("handles 59 minutes", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T14:59:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+59m"); + }); + + it("handles exactly 60 minutes as 1 hour", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T15:00:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+1h"); + }); + + it("handles 61 minutes as 1 hour 1 minute", () => { + const start = Math.floor(new Date("2025-01-15T14:00:00").getTime() / 1000); + const end = Math.floor(new Date("2025-01-15T15:01:00").getTime() / 1000); + + const result = formatFlexTimeDisplay(start, end); + + expect(result).toBe("+1h 1m"); + }); +}); diff --git a/app/features/scrims/scrims-utils.ts b/app/features/scrims/scrims-utils.ts index 2a00838b7..469678237 100644 --- a/app/features/scrims/scrims-utils.ts +++ b/app/features/scrims/scrims-utils.ts @@ -1,4 +1,7 @@ +import { differenceInMinutes } from "date-fns"; import * as R from "remeda"; +import { databaseTimestampToDate } from "~/utils/dates"; +import * as Scrim from "./core/Scrim"; import type { LutiDiv, ScrimPost } from "./scrims-types"; export const getPostRequestCensor = @@ -20,16 +23,17 @@ export const getPostRequestCensor = export function dividePosts(posts: Array, userId?: number) { const grouped = R.groupBy(posts, (post) => { - if (post.users.some((user) => user.id === userId)) { - return "OWNED"; + const isAccepted = post.requests.some((request) => request.isAccepted); + const isParticipating = userId + ? Scrim.isParticipating(post, userId) + : false; + + if (isAccepted && isParticipating) { + return "BOOKED"; } - if ( - post.requests.some((request) => - request.users.some((user) => user.id === userId), - ) - ) { - return "REQUESTED"; + if (post.users.some((user) => user.id === userId)) { + return "OWNED"; } return "NEUTRAL"; @@ -37,8 +41,8 @@ export function dividePosts(posts: Array, userId?: number) { return { owned: grouped.OWNED ?? [], - requested: grouped.REQUESTED ?? [], neutral: grouped.NEUTRAL ?? [], + booked: grouped.BOOKED ?? [], }; } @@ -53,3 +57,56 @@ export const serializeLutiDiv = (div: LutiDiv): number => { return Number(div); }; + +export function generateTimeOptions(startDate: Date, endDate: Date): number[] { + const timestamps = new Set(); + + const clearSubMinutes = (date: Date) => { + const cleared = new Date(date); + cleared.setSeconds(0, 0); + return cleared; + }; + + timestamps.add(clearSubMinutes(startDate).getTime()); + timestamps.add(clearSubMinutes(endDate).getTime()); + + const currentDate = clearSubMinutes(startDate); + const minutes = currentDate.getMinutes(); + + if (minutes > 0 && minutes < 30) { + currentDate.setMinutes(30, 0, 0); + } else if (minutes > 30) { + currentDate.setHours(currentDate.getHours() + 1, 0, 0, 0); + } + + while (currentDate <= endDate) { + timestamps.add(currentDate.getTime()); + currentDate.setMinutes(currentDate.getMinutes() + 30); + } + + return Array.from(timestamps).sort((a, b) => a - b); +} + +export function formatFlexTimeDisplay( + startTimestamp: number, + endTimestamp: number, +): string | null { + const totalMinutes = differenceInMinutes( + databaseTimestampToDate(endTimestamp), + databaseTimestampToDate(startTimestamp), + ); + const hours = Math.floor(totalMinutes / 60); + const remainingMinutes = totalMinutes % 60; + + if (hours > 0 && remainingMinutes > 0) { + return `+${hours}h ${remainingMinutes}m`; + } + if (hours > 0) { + return `+${hours}h`; + } + if (totalMinutes > 0) { + return `+${totalMinutes}m`; + } + + return null; +} diff --git a/app/features/settings/actions/settings.server.ts b/app/features/settings/actions/settings.server.ts index 1f1b02aaa..dd2843a05 100644 --- a/app/features/settings/actions/settings.server.ts +++ b/app/features/settings/actions/settings.server.ts @@ -1,5 +1,6 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { requireUser } from "~/features/auth/core/user.server"; +import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { parseRequestPayload } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; @@ -25,6 +26,13 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); break; } + case "UPDATE_NO_SCREEN": { + await QSettingsRepository.updateNoScreen({ + userId: user.id, + noScreen: Number(data.newValue), + }); + break; + } case "PLACEHOLDER": { break; } diff --git a/app/features/settings/loaders/settings.server.ts b/app/features/settings/loaders/settings.server.ts new file mode 100644 index 000000000..c42477bd2 --- /dev/null +++ b/app/features/settings/loaders/settings.server.ts @@ -0,0 +1,13 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { getUserId } from "~/features/auth/core/user.server"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const user = await getUserId(request); + + return { + noScreen: user + ? await UserRepository.anyUserPrefersNoScreen([user.id]) + : null, + }; +}; diff --git a/app/features/settings/routes/settings.tsx b/app/features/settings/routes/settings.tsx index 9d3f65ecb..891ed93ae 100644 --- a/app/features/settings/routes/settings.tsx +++ b/app/features/settings/routes/settings.tsx @@ -1,5 +1,10 @@ import type { MetaFunction } from "@remix-run/node"; -import { useFetcher, useNavigate, useSearchParams } from "@remix-run/react"; +import { + useFetcher, + useLoaderData, + useNavigate, + useSearchParams, +} from "@remix-run/react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { SendouSwitch } from "~/components/elements/Switch"; @@ -7,7 +12,6 @@ import { FormMessage } from "~/components/FormMessage"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import { useUser } from "~/features/auth/core/user"; -import { FF_SCRIMS_ENABLED } from "~/features/scrims/scrims-constants"; import { Theme, useTheme } from "~/features/theme/core/provider"; import { languages } from "~/modules/i18n/config"; import { metaTags } from "~/utils/remix"; @@ -15,9 +19,9 @@ import type { SendouRouteHandle } from "~/utils/remix.server"; import { navIconUrl, SETTINGS_PAGE } from "~/utils/urls"; import { SendouButton } from "../../../components/elements/Button"; import { SendouPopover } from "../../../components/elements/Popover"; - import { action } from "../actions/settings.server"; -export { action }; +import { loader } from "../loaders/settings.server"; +export { loader, action }; export const handle: SendouRouteHandle = { breadcrumb: () => ({ @@ -28,6 +32,7 @@ export const handle: SendouRouteHandle = { }; export default function SettingsPage() { + const data = useLoaderData(); const user = useUser(); const { t } = useTranslation(["common"]); @@ -53,20 +58,24 @@ export default function SettingsPage() { "common:settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText", )} /> - {FF_SCRIMS_ENABLED ? ( - - ) : null} + + ) : null} diff --git a/app/features/settings/settings-schemas.ts b/app/features/settings/settings-schemas.ts index b5dcc9519..22ee93a1e 100644 --- a/app/features/settings/settings-schemas.ts +++ b/app/features/settings/settings-schemas.ts @@ -10,6 +10,10 @@ export const settingsEditSchema = z.union([ _action: _action("DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED"), newValue: z.boolean(), }), + z.object({ + _action: _action("UPDATE_NO_SCREEN"), + newValue: z.boolean(), + }), z.object({ _action: _action("PLACEHOLDER"), }), diff --git a/app/features/tournament-bracket/components/StartedMatch.tsx b/app/features/tournament-bracket/components/StartedMatch.tsx index 193fdb9f4..113190309 100644 --- a/app/features/tournament-bracket/components/StartedMatch.tsx +++ b/app/features/tournament-bracket/components/StartedMatch.tsx @@ -160,11 +160,9 @@ export function StartedMatch({ bestOf: data.match.bestOf, })}
, - tournament.ctx.settings.enableNoScreenToggle ? ( - team.noScreen)} - /> + tournament.ctx.settings.enableNoScreenToggle && + typeof data.noScreen === "boolean" ? ( + ) : null, ]; diff --git a/app/features/tournament-bracket/core/summarizer.test.ts b/app/features/tournament-bracket/core/summarizer.test.ts index 5ca6af6f3..809905769 100644 --- a/app/features/tournament-bracket/core/summarizer.test.ts +++ b/app/features/tournament-bracket/core/summarizer.test.ts @@ -34,7 +34,6 @@ describe("tournamentSummary()", () => { name: `Team ${teamId}`, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, seed: 1, activeRosterUserIds: [], diff --git a/app/features/tournament-bracket/core/tests/mocks-li.ts b/app/features/tournament-bracket/core/tests/mocks-li.ts index 1e370d78a..2c10dfb87 100644 --- a/app/features/tournament-bracket/core/tests/mocks-li.ts +++ b/app/features/tournament-bracket/core/tests/mocks-li.ts @@ -7573,7 +7573,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Inkbound", seed: 1, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -7663,7 +7662,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Jet Lag!", seed: 2, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -7753,7 +7751,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Fearless", seed: 3, prefersNotToHost: 1, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -7843,7 +7840,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Remix", seed: 4, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -7921,7 +7917,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Star Allies", seed: 5, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8006,7 +8001,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "REWIND!!!", seed: 6, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -8096,7 +8090,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "FlipSide", seed: 7, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8186,7 +8179,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "o7 Honor Bound", seed: 8, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8288,7 +8280,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "I love Latinas Sonic", seed: 9, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8390,7 +8381,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Tidy Tidings", seed: 10, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -8468,7 +8458,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Los Inklings", seed: 11, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8570,7 +8559,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Celestial", seed: 12, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8672,7 +8660,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Serves up", seed: 13, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8762,7 +8749,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Ghistra", seed: 14, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -8852,7 +8838,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Moonshine", seed: 15, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -8954,7 +8939,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Inkception", seed: 16, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9044,7 +9028,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "FREAKFORCE", seed: 17, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9146,7 +9129,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Asphyxiation", seed: 18, prefersNotToHost: 1, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -9236,7 +9218,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Malicious Misery", seed: 19, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9326,7 +9307,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Magic Beans", seed: 20, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9398,7 +9378,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Distortion", seed: 21, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9476,7 +9455,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Ball up top", seed: 22, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9549,7 +9527,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Caniac Central", seed: 23, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9634,7 +9611,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Exam Week", seed: 24, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9719,7 +9695,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "THIS IS FINE", seed: 25, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9809,7 +9784,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Calamity Forge Neo", seed: 26, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9899,7 +9873,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Twister Time", seed: 27, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -9989,7 +9962,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "11 ft 8 Bridge", seed: 28, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10062,7 +10034,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Loud Noises", seed: 29, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10152,7 +10123,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Squid Rollups", seed: 30, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10230,7 +10200,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "✨ Cooler High ✨", seed: 31, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10320,7 +10289,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Event Horizon", seed: 32, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10398,7 +10366,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Turbo Torben", seed: 33, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10494,7 +10461,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Red Velvet Sea", seed: 34, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10572,7 +10538,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "TAMU Maroon", seed: 35, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10662,7 +10627,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Error 404", seed: 36, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10752,7 +10716,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "XName", seed: 37, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10854,7 +10817,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Brigada Woomy", seed: 38, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -10944,7 +10906,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Tidal Tempest", seed: 39, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11022,7 +10983,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Pringle Cat", seed: 40, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11112,7 +11072,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Cod in 4K", seed: 41, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11214,7 +11173,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "seasick", seed: 42, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11292,7 +11250,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "squid squad", seed: 43, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11377,7 +11334,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Sheer Cold", seed: 44, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11467,7 +11423,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "idiots in chat", seed: 45, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11569,7 +11524,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Deep Sea International", seed: 46, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11647,7 +11601,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "octo rehab", seed: 47, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11725,7 +11678,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "The Slam Blitzers", seed: 48, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11815,7 +11767,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Special Force", seed: 49, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11887,7 +11838,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Octaves", seed: 50, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -11989,7 +11939,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Tide Breaker", seed: 51, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -12091,7 +12040,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Nah I'd ink", seed: 52, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -12164,7 +12112,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Inferno", seed: 53, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -12242,7 +12189,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Moonslice", seed: 54, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -12332,7 +12278,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "SplatSCAD-Orange", seed: 55, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -12434,7 +12379,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Reignfall", seed: 56, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -12512,7 +12456,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Cloud Strikers!", seed: 57, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -12585,7 +12528,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Overdive", seed: 58, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -12675,7 +12617,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Fish Paste", seed: 59, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -12753,7 +12694,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Gubi Fortnite?!", seed: 60, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -12850,7 +12790,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Krill Streak", seed: 61, prefersNotToHost: 1, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -12940,7 +12879,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "EWfish", seed: 62, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13030,7 +12968,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Starfish★", seed: 63, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13127,7 +13064,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Zero Magnum", seed: 64, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13229,7 +13165,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Citronnade", seed: 65, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -13307,7 +13242,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Milkyway", seed: 66, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13397,7 +13331,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Crisis Averted", seed: 67, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13475,7 +13408,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "The British Empire", seed: 68, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13565,7 +13497,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Pixel per ink", seed: 69, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13655,7 +13586,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Paladins", seed: 70, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13745,7 +13675,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Baguette squad", seed: 71, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13835,7 +13764,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Job Application", seed: 73, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -13913,7 +13841,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Cosmic Hunters", seed: 74, prefersNotToHost: 1, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -14003,7 +13930,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Tocinitos de cielo", seed: 75, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14088,7 +14014,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Chirpy Chips Crusaders", seed: 76, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -14166,7 +14091,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Abyss Ink", seed: 77, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14238,7 +14162,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Six-Pack Yokes", seed: 78, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14328,7 +14251,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Squid's Rendezvous", seed: 79, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -14406,7 +14328,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "morning struggles", seed: 80, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -14484,7 +14405,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "squee-g simulator", seed: 81, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14563,7 +14483,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Luminaria", seed: 82, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14647,7 +14566,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "C-₅₀", seed: 83, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14725,7 +14643,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Bad at Math", seed: 84, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14798,7 +14715,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Starstrikerz", seed: 85, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, startingBracketIdx: null, @@ -14888,7 +14804,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "[Name Pending]", seed: 86, prefersNotToHost: 0, - noScreen: 0, droppedOut: 1, inviteCode: null, startingBracketIdx: null, @@ -14966,7 +14881,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "TAMU White", seed: 87, prefersNotToHost: 0, - noScreen: 1, droppedOut: 1, inviteCode: null, startingBracketIdx: null, diff --git a/app/features/tournament-bracket/core/tests/mocks-sos.ts b/app/features/tournament-bracket/core/tests/mocks-sos.ts index 4eaba920d..f70e87842 100644 --- a/app/features/tournament-bracket/core/tests/mocks-sos.ts +++ b/app/features/tournament-bracket/core/tests/mocks-sos.ts @@ -2439,7 +2439,6 @@ export const SWIM_OR_SINK_167 = ( name: "SOS WARRIORS", seed: 1, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730771673, @@ -2556,7 +2555,6 @@ export const SWIM_OR_SINK_167 = ( name: "/rw3", seed: 2, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730931681, @@ -2649,7 +2647,6 @@ export const SWIM_OR_SINK_167 = ( name: "Honkai", seed: 3, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730864603, @@ -2754,7 +2751,6 @@ export const SWIM_OR_SINK_167 = ( name: "🦌", seed: 4, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730932511, @@ -2847,7 +2843,6 @@ export const SWIM_OR_SINK_167 = ( name: "cutie patooties", seed: 5, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730922495, @@ -2940,7 +2935,6 @@ export const SWIM_OR_SINK_167 = ( name: "Triggerfish Zones Supremacy", seed: 6, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730788095, @@ -3062,7 +3056,6 @@ export const SWIM_OR_SINK_167 = ( name: "Retro Records", seed: 7, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730774561, @@ -3172,7 +3165,6 @@ export const SWIM_OR_SINK_167 = ( name: "Impulse", seed: 8, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730935243, @@ -3282,7 +3274,6 @@ export const SWIM_OR_SINK_167 = ( name: "Wave breaker", seed: 9, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730851309, @@ -3380,7 +3371,6 @@ export const SWIM_OR_SINK_167 = ( name: "FreeFlow", seed: 10, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730870818, @@ -3485,7 +3475,6 @@ export const SWIM_OR_SINK_167 = ( name: "Poisonous Gas Balloon", seed: 11, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730872875, @@ -3590,7 +3579,6 @@ export const SWIM_OR_SINK_167 = ( name: "😺", seed: 12, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730934390, @@ -3683,7 +3671,6 @@ export const SWIM_OR_SINK_167 = ( name: "petrichor.", seed: 13, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730925172, @@ -3793,7 +3780,6 @@ export const SWIM_OR_SINK_167 = ( name: "New Wave", seed: 14, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730526186, @@ -3915,7 +3901,6 @@ export const SWIM_OR_SINK_167 = ( name: "Roblox Fishermen", seed: 15, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730940438, @@ -3970,7 +3955,6 @@ export const SWIM_OR_SINK_167 = ( name: "2 chicken quesadillas", seed: 16, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730912708, @@ -4063,7 +4047,6 @@ export const SWIM_OR_SINK_167 = ( name: "REBOUND", seed: 17, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730936919, @@ -4156,7 +4139,6 @@ export const SWIM_OR_SINK_167 = ( name: "hey wanna see something cool", seed: 18, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730937337, @@ -4249,7 +4231,6 @@ export const SWIM_OR_SINK_167 = ( name: "Sweet Espresso", seed: 19, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730769135, @@ -4371,7 +4352,6 @@ export const SWIM_OR_SINK_167 = ( name: "Outcast", seed: 20, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730836875, @@ -4481,7 +4461,6 @@ export const SWIM_OR_SINK_167 = ( name: "Memento Moray", seed: 21, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730844165, @@ -4579,7 +4558,6 @@ export const SWIM_OR_SINK_167 = ( name: "moi et la gothique", seed: 22, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730926359, @@ -4672,7 +4650,6 @@ export const SWIM_OR_SINK_167 = ( name: "the fire worms are back", seed: 23, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730928135, @@ -4765,7 +4742,6 @@ export const SWIM_OR_SINK_167 = ( name: "Meow's the Chance", seed: 24, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730880730, @@ -4849,7 +4825,6 @@ export const SWIM_OR_SINK_167 = ( name: "Shellfire", seed: 25, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730939363, @@ -4959,7 +4934,6 @@ export const SWIM_OR_SINK_167 = ( name: "Eepy Dreepy", seed: 26, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730838132, @@ -5057,7 +5031,6 @@ export const SWIM_OR_SINK_167 = ( name: "Blitz Wave", seed: 27, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730917380, @@ -5105,7 +5078,6 @@ export const SWIM_OR_SINK_167 = ( name: "YT->MP3", seed: 28, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730832667, @@ -5198,7 +5170,6 @@ export const SWIM_OR_SINK_167 = ( name: "Wavelength", seed: 29, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730853887, @@ -5308,7 +5279,6 @@ export const SWIM_OR_SINK_167 = ( name: "fearless I think", seed: 30, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730928986, @@ -5413,7 +5383,6 @@ export const SWIM_OR_SINK_167 = ( name: "Los Inklings", seed: 31, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730775625, @@ -5511,7 +5480,6 @@ export const SWIM_OR_SINK_167 = ( name: "High Point", seed: 32, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730605037, @@ -5621,7 +5589,6 @@ export const SWIM_OR_SINK_167 = ( name: "Magic Beans", seed: 33, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730862741, @@ -5731,7 +5698,6 @@ export const SWIM_OR_SINK_167 = ( name: "Sonora Inkamita de Iztapalapa", seed: 34, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730840221, @@ -5815,7 +5781,6 @@ export const SWIM_OR_SINK_167 = ( name: "Squid Rollups", seed: 35, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730770163, @@ -5937,7 +5902,6 @@ export const SWIM_OR_SINK_167 = ( name: "Strawberry Supernova", seed: 36, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730753582, @@ -6009,7 +5973,6 @@ export const SWIM_OR_SINK_167 = ( name: "Moonshine", seed: 37, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730929508, @@ -6107,7 +6070,6 @@ export const SWIM_OR_SINK_167 = ( name: "SSSoda!!!", seed: 38, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730918770, @@ -6174,7 +6136,6 @@ export const SWIM_OR_SINK_167 = ( name: "Lock out", seed: 39, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730851475, @@ -6267,7 +6228,6 @@ export const SWIM_OR_SINK_167 = ( name: "Distortion", seed: 40, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730726309, @@ -6351,7 +6311,6 @@ export const SWIM_OR_SINK_167 = ( name: "California girls", seed: 41, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730840643, @@ -6456,7 +6415,6 @@ export const SWIM_OR_SINK_167 = ( name: "Anenemies", seed: 42, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730827259, @@ -6566,7 +6524,6 @@ export const SWIM_OR_SINK_167 = ( name: "Overdive", seed: 43, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730863477, @@ -6688,7 +6645,6 @@ export const SWIM_OR_SINK_167 = ( name: "The Coastal Service", seed: 44, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730907528, @@ -6781,7 +6737,6 @@ export const SWIM_OR_SINK_167 = ( name: "Fanshawe Fuel", seed: 45, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730932702, @@ -6886,7 +6841,6 @@ export const SWIM_OR_SINK_167 = ( name: "Loud Noises", seed: 46, prefersNotToHost: 1, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730832003, @@ -6996,7 +6950,6 @@ export const SWIM_OR_SINK_167 = ( name: "Yarg", seed: 47, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730938507, @@ -7089,7 +7042,6 @@ export const SWIM_OR_SINK_167 = ( name: "HP OfficeJet All-In-One Printer", seed: 48, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730739211, @@ -7206,7 +7158,6 @@ export const SWIM_OR_SINK_167 = ( name: "C-Dragons", seed: 49, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1730857328, @@ -7316,7 +7267,6 @@ export const SWIM_OR_SINK_167 = ( name: "United Crushers", seed: 50, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730591675, @@ -7388,7 +7338,6 @@ export const SWIM_OR_SINK_167 = ( name: "Hal Laboratory Dog Eggs", seed: 51, prefersNotToHost: 1, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730859986, @@ -7481,7 +7430,6 @@ export const SWIM_OR_SINK_167 = ( name: "Ultra Kraken", seed: 52, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1730703689, diff --git a/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts b/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts index 8bcc22fb6..e5361e06c 100644 --- a/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts +++ b/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts @@ -401,7 +401,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "Bamboo Pirates", seed: 1, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1734656039, @@ -491,7 +490,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "the usual suspects", seed: 2, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1734423187, @@ -564,7 +562,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "ayam goreng", seed: 3, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1734660846, @@ -637,7 +634,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "Fruitea!", seed: 5, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1734683349, @@ -727,7 +723,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "The Huh Inkqisition", seed: 6, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1734608907, @@ -824,7 +819,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "Monkey Barrel", seed: 7, prefersNotToHost: 0, - noScreen: 1, droppedOut: 0, inviteCode: null, createdAt: 1734397954, @@ -914,7 +908,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({ name: "Ras+1", seed: 8, prefersNotToHost: 0, - noScreen: 0, droppedOut: 0, inviteCode: null, createdAt: 1734598652, diff --git a/app/features/tournament-bracket/core/tests/mocks.ts b/app/features/tournament-bracket/core/tests/mocks.ts index a69f77ad5..fead35e04 100644 --- a/app/features/tournament-bracket/core/tests/mocks.ts +++ b/app/features/tournament-bracket/core/tests/mocks.ts @@ -2106,7 +2106,6 @@ export const PADDLING_POOL_257 = () => seed: 1, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2221,7 +2220,6 @@ export const PADDLING_POOL_257 = () => seed: 2, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2348,7 +2346,6 @@ export const PADDLING_POOL_257 = () => seed: 3, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2463,7 +2460,6 @@ export const PADDLING_POOL_257 = () => seed: 4, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2578,7 +2574,6 @@ export const PADDLING_POOL_257 = () => seed: 5, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2703,7 +2698,6 @@ export const PADDLING_POOL_257 = () => seed: 6, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2828,7 +2822,6 @@ export const PADDLING_POOL_257 = () => seed: 7, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -2952,7 +2945,6 @@ export const PADDLING_POOL_257 = () => seed: 8, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3064,7 +3056,6 @@ export const PADDLING_POOL_257 = () => seed: 9, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3202,7 +3193,6 @@ export const PADDLING_POOL_257 = () => seed: 10, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3327,7 +3317,6 @@ export const PADDLING_POOL_257 = () => seed: 11, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3584,7 +3573,6 @@ export const PADDLING_POOL_257 = () => seed: 13, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3708,7 +3696,6 @@ export const PADDLING_POOL_257 = () => seed: 14, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3843,7 +3830,6 @@ export const PADDLING_POOL_257 = () => seed: 15, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -3959,7 +3945,6 @@ export const PADDLING_POOL_257 = () => seed: 16, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -4075,7 +4060,6 @@ export const PADDLING_POOL_257 = () => seed: 17, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -4318,7 +4302,6 @@ export const PADDLING_POOL_257 = () => seed: 19, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -4542,7 +4525,6 @@ export const PADDLING_POOL_257 = () => seed: 21, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -4653,7 +4635,6 @@ export const PADDLING_POOL_257 = () => seed: 22, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -4764,7 +4745,6 @@ export const PADDLING_POOL_257 = () => seed: 23, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -4880,7 +4860,6 @@ export const PADDLING_POOL_257 = () => seed: 24, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5121,7 +5100,6 @@ export const PADDLING_POOL_257 = () => seed: 26, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5249,7 +5227,6 @@ export const PADDLING_POOL_257 = () => seed: 27, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5372,7 +5349,6 @@ export const PADDLING_POOL_257 = () => seed: 28, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5483,7 +5459,6 @@ export const PADDLING_POOL_257 = () => seed: 29, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5731,7 +5706,6 @@ export const PADDLING_POOL_257 = () => seed: 31, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5842,7 +5816,6 @@ export const PADDLING_POOL_257 = () => seed: 32, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -5958,7 +5931,6 @@ export const PADDLING_POOL_257 = () => seed: 33, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -6086,7 +6058,6 @@ export const PADDLING_POOL_257 = () => seed: 34, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -6197,7 +6168,6 @@ export const PADDLING_POOL_257 = () => seed: 35, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -8275,7 +8245,6 @@ export const PADDLING_POOL_255 = () => seed: 2, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -8390,7 +8359,6 @@ export const PADDLING_POOL_255 = () => seed: 3, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -8641,7 +8609,6 @@ export const PADDLING_POOL_255 = () => seed: 5, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -8756,7 +8723,6 @@ export const PADDLING_POOL_255 = () => seed: 6, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -8883,7 +8849,6 @@ export const PADDLING_POOL_255 = () => seed: 7, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -9108,7 +9073,6 @@ export const PADDLING_POOL_255 = () => seed: 9, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -9245,7 +9209,6 @@ export const PADDLING_POOL_255 = () => seed: 10, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -9477,7 +9440,6 @@ export const PADDLING_POOL_255 = () => seed: 12, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -9732,7 +9694,6 @@ export const PADDLING_POOL_255 = () => seed: 14, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -9843,7 +9804,6 @@ export const PADDLING_POOL_255 = () => seed: 15, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -9966,7 +9926,6 @@ export const PADDLING_POOL_255 = () => seed: 16, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -10077,7 +10036,6 @@ export const PADDLING_POOL_255 = () => seed: 17, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -10308,7 +10266,6 @@ export const PADDLING_POOL_255 = () => seed: 19, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -10431,7 +10388,6 @@ export const PADDLING_POOL_255 = () => seed: 20, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -10547,7 +10503,6 @@ export const PADDLING_POOL_255 = () => seed: 21, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -10675,7 +10630,6 @@ export const PADDLING_POOL_255 = () => seed: 22, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -10923,7 +10877,6 @@ export const PADDLING_POOL_255 = () => seed: 24, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11034,7 +10987,6 @@ export const PADDLING_POOL_255 = () => seed: 25, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11157,7 +11109,6 @@ export const PADDLING_POOL_255 = () => seed: 26, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11280,7 +11231,6 @@ export const PADDLING_POOL_255 = () => seed: 27, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11391,7 +11341,6 @@ export const PADDLING_POOL_255 = () => seed: 28, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11532,7 +11481,6 @@ export const PADDLING_POOL_255 = () => seed: 29, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11655,7 +11603,6 @@ export const PADDLING_POOL_255 = () => seed: 30, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -11783,7 +11730,6 @@ export const PADDLING_POOL_255 = () => seed: 31, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -12019,7 +11965,6 @@ export const PADDLING_POOL_255 = () => seed: 33, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -12135,7 +12080,6 @@ export const PADDLING_POOL_255 = () => seed: 34, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -12287,7 +12231,6 @@ export const PADDLING_POOL_255 = () => seed: 35, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -14519,7 +14462,6 @@ export const IN_THE_ZONE_32 = ({ seed: 1, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -14621,7 +14563,6 @@ export const IN_THE_ZONE_32 = ({ seed: 2, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -14723,7 +14664,6 @@ export const IN_THE_ZONE_32 = ({ seed: 3, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -14825,7 +14765,6 @@ export const IN_THE_ZONE_32 = ({ seed: 4, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -14953,7 +14892,6 @@ export const IN_THE_ZONE_32 = ({ seed: 5, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15068,7 +15006,6 @@ export const IN_THE_ZONE_32 = ({ seed: 6, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15183,7 +15120,6 @@ export const IN_THE_ZONE_32 = ({ seed: 7, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15311,7 +15247,6 @@ export const IN_THE_ZONE_32 = ({ seed: 8, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15439,7 +15374,6 @@ export const IN_THE_ZONE_32 = ({ seed: 9, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15541,7 +15475,6 @@ export const IN_THE_ZONE_32 = ({ seed: 10, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15643,7 +15576,6 @@ export const IN_THE_ZONE_32 = ({ seed: 11, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15745,7 +15677,6 @@ export const IN_THE_ZONE_32 = ({ seed: 12, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15847,7 +15778,6 @@ export const IN_THE_ZONE_32 = ({ seed: 13, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -15962,7 +15892,6 @@ export const IN_THE_ZONE_32 = ({ seed: 14, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16064,7 +15993,6 @@ export const IN_THE_ZONE_32 = ({ seed: 15, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16165,7 +16093,6 @@ export const IN_THE_ZONE_32 = ({ seed: 16, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16280,7 +16207,6 @@ export const IN_THE_ZONE_32 = ({ seed: 17, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16400,7 +16326,6 @@ export const IN_THE_ZONE_32 = ({ seed: 18, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16515,7 +16440,6 @@ export const IN_THE_ZONE_32 = ({ seed: 19, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16643,7 +16567,6 @@ export const IN_THE_ZONE_32 = ({ seed: 20, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16745,7 +16668,6 @@ export const IN_THE_ZONE_32 = ({ seed: 21, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16876,7 +16798,6 @@ export const IN_THE_ZONE_32 = ({ seed: 22, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -16985,7 +16906,6 @@ export const IN_THE_ZONE_32 = ({ seed: 23, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17084,7 +17004,6 @@ export const IN_THE_ZONE_32 = ({ seed: 24, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17184,7 +17103,6 @@ export const IN_THE_ZONE_32 = ({ seed: 25, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17306,7 +17224,6 @@ export const IN_THE_ZONE_32 = ({ seed: 26, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17416,7 +17333,6 @@ export const IN_THE_ZONE_32 = ({ seed: 27, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17532,7 +17448,6 @@ export const IN_THE_ZONE_32 = ({ seed: 28, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17642,7 +17557,6 @@ export const IN_THE_ZONE_32 = ({ seed: 29, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -17871,7 +17785,6 @@ export const IN_THE_ZONE_32 = ({ seed: 31, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -18082,7 +17995,6 @@ export const IN_THE_ZONE_32 = ({ seed: 33, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, @@ -18199,7 +18111,6 @@ export const IN_THE_ZONE_32 = ({ seed: 34, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, team: null, inviteCode: null, avgSeedingSkillOrdinal: null, diff --git a/app/features/tournament-bracket/core/tests/test-utils.ts b/app/features/tournament-bracket/core/tests/test-utils.ts index b6a4ddb1d..613a5871f 100644 --- a/app/features/tournament-bracket/core/tests/test-utils.ts +++ b/app/features/tournament-bracket/core/tests/test-utils.ts @@ -23,7 +23,6 @@ export const tournamentCtxTeam = ( name: `Team ${teamId}`, prefersNotToHost: 0, droppedOut: 0, - noScreen: 0, seed: teamId + 1, ...partial, }; diff --git a/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts index 3cfde459e..1d7bb6364 100644 --- a/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts @@ -1,6 +1,9 @@ +import cachified from "@epic-web/cachified"; import type { LoaderFunctionArgs } from "@remix-run/node"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; +import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server"; import { logger } from "~/utils/logger"; import { notFoundIfFalsy, parseParams } from "~/utils/remix.server"; import { resolveMapList } from "../core/mapList.server"; @@ -27,6 +30,22 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { ? await TournamentRepository.pickBanEventsByMatchId(match.id) : []; + // cached so that some user changing their noScreen preference doesn't + // change the selection once the match has started + const noScreen = + match.opponentOne?.id && match.opponentTwo?.id + ? await cachified({ + key: `no-screen-mid-${matchId}`, + cache, + ttl: ttl(IN_MILLISECONDS.TWO_DAYS), + async getFreshValue() { + return UserRepository.anyUserPrefersNoScreen( + match.players.map((p) => p.id), + ); + }, + }) + : null; + const mapList = match.opponentOne?.id && match.opponentTwo?.id ? resolveMapList({ @@ -55,5 +74,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { matchIsOver: match.opponentOne?.result === "win" || match.opponentTwo?.result === "win", + noScreen, }; }; diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts index 68c012a76..f0fce40bd 100644 --- a/app/features/tournament/TournamentRepository.server.ts +++ b/app/features/tournament/TournamentRepository.server.ts @@ -163,7 +163,6 @@ export async function findById(id: number) { "TournamentTeam.name", "TournamentTeam.seed", "TournamentTeam.prefersNotToHost", - "TournamentTeam.noScreen", "TournamentTeam.droppedOut", "TournamentTeam.inviteCode", "TournamentTeam.createdAt", @@ -1136,3 +1135,54 @@ export function deleteSwissMatches({ .where("roundId", "=", roundId) .execute(); } + +export async function searchByName({ + query, + limit, + minStartTime, +}: { + query: string; + limit: number; + minStartTime?: Date; +}) { + let sqlQuery = db + .selectFrom("Tournament") + .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") + .innerJoin( + "CalendarEventDate", + "CalendarEvent.id", + "CalendarEventDate.eventId", + ) + .leftJoin( + "UnvalidatedUserSubmittedImage", + "CalendarEvent.avatarImgId", + "UnvalidatedUserSubmittedImage.id", + ) + .select([ + "Tournament.id", + "CalendarEvent.name", + "CalendarEventDate.startTime", + "UnvalidatedUserSubmittedImage.url as logoUrl", + ]) + .where("CalendarEvent.name", "like", `%${query}%`) + .where("CalendarEvent.hidden", "=", 0) + .orderBy("CalendarEventDate.startTime", "desc") + .limit(limit); + + if (minStartTime) { + sqlQuery = sqlQuery.where( + "CalendarEventDate.startTime", + ">=", + dateToDatabaseTimestamp(minStartTime), + ); + } + + const results = await sqlQuery.execute(); + + return results.map((result) => ({ + ...result, + logoSrc: result.logoUrl + ? userSubmittedImage(result.logoUrl) + : HACKY_resolvePicture({ name: result.name }), + })); +} diff --git a/app/features/tournament/TournamentTeamRepository.server.ts b/app/features/tournament/TournamentTeamRepository.server.ts index e34fa3154..cd427440c 100644 --- a/app/features/tournament/TournamentTeamRepository.server.ts +++ b/app/features/tournament/TournamentTeamRepository.server.ts @@ -115,10 +115,7 @@ export function create({ tournamentId, ownerInGameName, }: { - team: Pick< - Tables["TournamentTeam"], - "name" | "prefersNotToHost" | "noScreen" | "teamId" - >; + team: Pick; avatarFileName?: string; userId: number; tournamentId: number; @@ -140,7 +137,6 @@ export function create({ name: team.name, inviteCode: shortNanoid(), prefersNotToHost: team.prefersNotToHost, - noScreen: team.noScreen, teamId: team.teamId, avatarImgId, }) @@ -179,7 +175,6 @@ export function copyFromAnotherTournament({ "TournamentTeam.avatarImgId", "TournamentTeam.createdAt", "TournamentTeam.name", - "TournamentTeam.noScreen", "TournamentTeam.prefersNotToHost", "TournamentTeam.teamId", @@ -270,7 +265,7 @@ export function update({ }: { team: Pick< Tables["TournamentTeam"], - "id" | "name" | "prefersNotToHost" | "noScreen" | "teamId" + "id" | "name" | "prefersNotToHost" | "teamId" >; avatarFileName?: string; userId: number; @@ -291,7 +286,6 @@ export function update({ .set({ name: team.name, prefersNotToHost: team.prefersNotToHost, - noScreen: team.noScreen, teamId: team.teamId, avatarImgId, }) diff --git a/app/features/tournament/actions/to.$id.admin.server.ts b/app/features/tournament/actions/to.$id.admin.server.ts index a8f00ab56..853363a98 100644 --- a/app/features/tournament/actions/to.$id.admin.server.ts +++ b/app/features/tournament/actions/to.$id.admin.server.ts @@ -65,7 +65,6 @@ export const action: ActionFunction = async ({ request, params }) => { }), team: { name: data.teamName, - noScreen: 0, prefersNotToHost: 0, teamId: null, }, diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts index 5c3db0ea7..ee367f5da 100644 --- a/app/features/tournament/actions/to.$id.register.server.ts +++ b/app/features/tournament/actions/to.$id.register.server.ts @@ -90,7 +90,6 @@ export const action: ActionFunction = async ({ request, params }) => { id: ownTeam.id, name: data.teamName, prefersNotToHost: Number(data.prefersNotToHost), - noScreen: Number(data.noScreen), teamId: data.teamId ?? null, }, }); @@ -125,7 +124,6 @@ export const action: ActionFunction = async ({ request, params }) => { }), team: { name: data.teamName, - noScreen: Number(data.noScreen), prefersNotToHost: Number(data.prefersNotToHost), teamId: data.teamId ?? null, }, diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index ce7af629a..ee4e2e94a 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -819,21 +819,6 @@ function TeamInfo({ {t("tournament:pre.info.noHost")} - - {tournament.ctx.settings.enableNoScreenToggle ? ( -
- - -
- ) : null} ; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const user = await getUserId(request); + if (!user) { + return []; + } + + const { + q: query, + limit, + minStartTime, + } = parseSearchParams({ + request, + schema: tournamentSearchSearchParamsSchema, + }); + + if (!query) return []; + + return { + tournaments: await TournamentRepository.searchByName({ + query, + limit, + minStartTime, + }), + query, + }; +}; diff --git a/app/features/tournament/tournament-schemas.server.ts b/app/features/tournament/tournament-schemas.server.ts index c91f6831d..8a579ee28 100644 --- a/app/features/tournament/tournament-schemas.server.ts +++ b/app/features/tournament/tournament-schemas.server.ts @@ -23,7 +23,6 @@ export const registerSchema = z.union([ _action: _action("UPSERT_TEAM"), teamName, prefersNotToHost: z.preprocess(checkboxValueToBoolean, z.boolean()), - noScreen: z.preprocess(checkboxValueToBoolean, z.boolean()), teamId: optionalId, }), z.object({ @@ -78,6 +77,12 @@ export const joinSchema = z.object({ trust: z.preprocess(checkboxValueToBoolean, z.boolean()), }); +export const tournamentSearchSearchParamsSchema = z.object({ + q: z.string().max(100), + limit: z.coerce.number().int().min(1).max(25).catch(25), + minStartTime: z.coerce.date().optional().catch(undefined), +}); + export const adminActionSchema = z.union([ z.object({ _action: _action("CHANGE_TEAM_OWNER"), diff --git a/app/features/tournament/tournament-test-utils.ts b/app/features/tournament/tournament-test-utils.ts index d1785a183..510a3b209 100644 --- a/app/features/tournament/tournament-test-utils.ts +++ b/app/features/tournament/tournament-test-utils.ts @@ -62,7 +62,6 @@ export async function dbInsertTournamentTeam({ ownerInGameName: null, team: { name: `Test Team ${ownerId}`, - noScreen: 0, prefersNotToHost: 0, teamId: null, }, diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index c0a2ba1da..459114d96 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -1050,3 +1050,18 @@ export const updateMany = dbDirect.transaction( } }, ); + +export async function anyUserPrefersNoScreen( + userIds: number[], +): Promise { + if (userIds.length === 0) return false; + + const result = await db + .selectFrom("User") + .select("User.noScreen") + .where("User.id", "in", userIds) + .where("User.noScreen", "=", 1) + .executeTakeFirst(); + + return Boolean(result); +} diff --git a/app/routes.ts b/app/routes.ts index b5c60e9d2..54b035558 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -85,6 +85,7 @@ export default [ "features/object-damage-calculator/routes/object-damage-calculator.tsx", ), + route("/to/search", "features/tournament/routes/to.search.ts"), route("/to/:id", "features/tournament/routes/to.$id.tsx", [ index("features/tournament/routes/to.$id.index.ts"), route("register", "features/tournament/routes/to.$id.register.tsx"), diff --git a/app/routines/list.server.ts b/app/routines/list.server.ts index 01f22a7c9..f794538c9 100644 --- a/app/routines/list.server.ts +++ b/app/routines/list.server.ts @@ -3,6 +3,7 @@ import { DeleteOldNotificationsRoutine } from "./deleteOldNotifications"; import { DeleteOldTrustRoutine } from "./deleteOldTrusts"; import { NotifyCheckInStartRoutine } from "./notifyCheckInStart"; import { NotifyPlusServerVotingRoutine } from "./notifyPlusServerVoting"; +import { NotifyScrimStartingSoonRoutine } from "./notifyScrimStartingSoon"; import { NotifySeasonStartRoutine } from "./notifySeasonStart"; import { SetOldGroupsAsInactiveRoutine } from "./setOldGroupsAsInactive"; import { UpdatePatreonDataRoutine } from "./updatePatreonData"; @@ -12,6 +13,7 @@ export const everyHourAt00 = [ NotifySeasonStartRoutine, NotifyPlusServerVotingRoutine, NotifyCheckInStartRoutine, + NotifyScrimStartingSoonRoutine, ]; /** List of Routines that should occur hourly at XX:30 */ diff --git a/app/routines/notifyScrimStartingSoon.ts b/app/routines/notifyScrimStartingSoon.ts new file mode 100644 index 000000000..436b490ec --- /dev/null +++ b/app/routines/notifyScrimStartingSoon.ts @@ -0,0 +1,42 @@ +import { add, sub } from "date-fns"; +import { notify } from "../features/notifications/core/notify.server"; +import * as Scrim from "../features/scrims/core/Scrim"; +import * as ScrimPostRepository from "../features/scrims/ScrimPostRepository.server"; +import { databaseTimestampToJavascriptTimestamp } from "../utils/dates"; +import { logger } from "../utils/logger"; +import { Routine } from "./routine.server"; + +export const NotifyScrimStartingSoonRoutine = new Routine({ + name: "NotifyScrimStartingSoon", + func: async () => { + const now = new Date(); + + const scrims = + await ScrimPostRepository.findAcceptedScrimsBetweenTwoTimestamps({ + startTime: now, + endTime: add(now, { hours: 1 }), + excludeRecentlyCreated: sub(now, { hours: 2 }), + }); + + for (const scrim of scrims) { + const participantIds = Scrim.participantIdsListFromAccepted(scrim); + + logger.info( + `Notifying scrim starting soon for scrim ${scrim.id} with ${participantIds.length} participants`, + ); + + await notify({ + notification: { + type: "SCRIM_STARTING_SOON", + meta: { + id: scrim.id, + at: databaseTimestampToJavascriptTimestamp( + Scrim.getStartTime(scrim), + ), + }, + }, + userIds: participantIds, + }); + } + }, +}); diff --git a/app/styles/common.css b/app/styles/common.css index b5acfe975..02f2719a7 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -122,6 +122,12 @@ input[type="checkbox"] { display: none !important; } +input[type="time"]::-webkit-calendar-picker-indicator { + filter: invert(1); + opacity: 0.55; + cursor: pointer; +} + label { display: block; font-size: var(--fonts-xs); @@ -208,6 +214,11 @@ select:disabled { background-image: url('data:image/svg+xml;utf8,'); } +.light input[type="time"]::-webkit-calendar-picker-indicator { + filter: invert(0); + opacity: 0.55; +} + select::selection { overflow: hidden; font-weight: bold; diff --git a/app/utils/cache.server.ts b/app/utils/cache.server.ts index fe96b054a..edd777ea0 100644 --- a/app/utils/cache.server.ts +++ b/app/utils/cache.server.ts @@ -29,4 +29,5 @@ export const IN_MILLISECONDS = { HALF_HOUR: 30 * 60 * 1000, ONE_HOUR: 60 * 60 * 1000, TWO_HOURS: 2 * 60 * 60 * 1000, + TWO_DAYS: 2 * 24 * 60 * 60 * 1000, }; diff --git a/app/utils/urls.ts b/app/utils/urls.ts index 842ce2693..4656be612 100644 --- a/app/utils/urls.ts +++ b/app/utils/urls.ts @@ -409,7 +409,7 @@ export const getWeaponUsage = ({ }; export const mapsPageWithMapPool = (mapPool: MapPool) => - `/maps?readonly&pool=${mapPool.serialized}`; + `${MAPS_URL}?readonly&pool=${mapPool.serialized}`; export const articlePage = (slug: string) => `${ARTICLES_MAIN_PAGE}/${slug}`; export const analyzerPage = (args?: { weaponId: MainWeaponId; diff --git a/app/utils/zod.test.ts b/app/utils/zod.test.ts index 7e7566646..abd67e018 100644 --- a/app/utils/zod.test.ts +++ b/app/utils/zod.test.ts @@ -3,6 +3,7 @@ import { actuallyNonEmptyStringOrNull, hasZalgo, normalizeFriendCode, + timeString, } from "./zod"; describe("normalizeFriendCode", () => { @@ -77,3 +78,45 @@ describe("actuallyNonEmptyStringOrNull", () => { expect(actuallyNonEmptyStringOrNull("󠀠󠀠󠀠󠀠󠀠")).toBeNull(); }); }); + +describe("timeString", () => { + it("accepts valid time in HH:MM format", () => { + expect(timeString.safeParse("00:00").success).toBe(true); + expect(timeString.safeParse("12:30").success).toBe(true); + expect(timeString.safeParse("23:59").success).toBe(true); + }); + + it("accepts times with leading zeros", () => { + expect(timeString.safeParse("01:05").success).toBe(true); + expect(timeString.safeParse("09:00").success).toBe(true); + }); + + it("rejects invalid hour values", () => { + expect(timeString.safeParse("24:00").success).toBe(false); + expect(timeString.safeParse("25:30").success).toBe(false); + expect(timeString.safeParse("99:00").success).toBe(false); + }); + + it("rejects invalid minute values", () => { + expect(timeString.safeParse("12:60").success).toBe(false); + expect(timeString.safeParse("12:99").success).toBe(false); + }); + + it("rejects malformed time strings", () => { + expect(timeString.safeParse("1:30").success).toBe(false); + expect(timeString.safeParse("12:3").success).toBe(false); + expect(timeString.safeParse("12-30").success).toBe(false); + expect(timeString.safeParse("1230").success).toBe(false); + expect(timeString.safeParse("12:30:00").success).toBe(false); + }); + + it("rejects non-string values", () => { + expect(timeString.safeParse(1230).success).toBe(false); + expect(timeString.safeParse(null).success).toBe(false); + expect(timeString.safeParse(undefined).success).toBe(false); + }); + + it("rejects empty string", () => { + expect(timeString.safeParse("").success).toBe(false); + }); +}); diff --git a/app/utils/zod.ts b/app/utils/zod.ts index 457830d51..429bdc1c1 100644 --- a/app/utils/zod.ts +++ b/app/utils/zod.ts @@ -32,6 +32,9 @@ export const dbBoolean = z.coerce.number().min(0).max(1).int(); const hexCodeRegex = /^#(?:[0-9a-fA-F]{3}){1,2}[0-9]{0,2}$/; // https://stackoverflow.com/a/1636354 export const hexCode = z.string().regex(hexCodeRegex); +const timeStringRegex = /^([01]\d|2[0-3]):([0-5]\d)$/; +export const timeString = z.string().regex(timeStringRegex); + const abilityNameToType = (val: string) => abilities.find((ability) => ability.name === val)?.type; export const headMainSlotAbility = z diff --git a/db-test.sqlite3 b/db-test.sqlite3 index c7296a873..dc4dcb82e 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/e2e/scrims.spec.ts b/e2e/scrims.spec.ts index 0dc4de9e1..8510f3998 100644 --- a/e2e/scrims.spec.ts +++ b/e2e/scrims.spec.ts @@ -54,6 +54,8 @@ test.describe("Scrims", () => { test("requests an existing scrim post & cancels the request", async ({ page, }) => { + const INITIAL_AVAILABLE_TO_REQUEST_COUNT = 15; + await seed(page); await impersonate(page, ADMIN_ID); await navigate({ @@ -61,20 +63,33 @@ test.describe("Scrims", () => { url: scrimsPage(), }); + const requestScrimButtonLocator = page.getByTestId("request-scrim-button"); + await page.getByTestId("available-scrims-tab").click(); - await page.getByRole("button", { name: "Request" }).first().click(); + await requestScrimButtonLocator.first().click(); await submit(page); - await page.getByTestId("requests-scrims-tab").click(); + await expect(requestScrimButtonLocator).toHaveCount( + INITIAL_AVAILABLE_TO_REQUEST_COUNT - 1, + ); - const cancelRequestButton = page.getByRole("button", { + const togglePendingRequestsButton = page.getByTestId( + "toggle-pending-requests-button", + ); + + await togglePendingRequestsButton.first().click(); + + await page.getByTestId("view-request-button").first().click(); + + const cancelButton = page.getByRole("button", { name: "Cancel", }); - expect(cancelRequestButton).toHaveCount(5); - await cancelRequestButton.first().click(); - await page.getByTestId("confirm-button").click(); - await expect(cancelRequestButton).toHaveCount(4); + await cancelButton.click(); + + await expect(requestScrimButtonLocator).toHaveCount( + INITIAL_AVAILABLE_TO_REQUEST_COUNT, + ); }); test("accepts a request", async ({ page }) => { @@ -85,10 +100,16 @@ test.describe("Scrims", () => { url: scrimsPage(), }); - await page.getByRole("button", { name: "Accept" }).first().click(); + await page.getByTestId("confirm-modal-trigger-button").first().click(); await page.getByTestId("confirm-button").click(); - await page.getByRole("link", { name: "Contact" }).click(); + await page.getByTestId("booked-scrims-tab").click(); + + const contactButtonLocator = page.getByRole("link", { name: "Contact" }); + + await expect(contactButtonLocator).toHaveCount(2); + + await page.getByRole("link", { name: "Contact" }).first().click(); await expect(page.getByText("Scheduled scrim")).toBeVisible(); }); @@ -102,10 +123,12 @@ test.describe("Scrims", () => { }); // Accept the first available scrim request to make it possible to access the scrim details page - await page.getByRole("button", { name: "Accept" }).first().click(); + await page.getByTestId("confirm-modal-trigger-button").first().click(); await page.getByTestId("confirm-button").click(); - await page.getByRole("link", { name: "Contact" }).click(); + await page.getByTestId("booked-scrims-tab").click(); + + await page.getByRole("link", { name: "Contact" }).first().click(); // Cancel the scrim await page.getByRole("button", { name: "Cancel" }).click(); diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index adfc8bcbf..2459b4d20 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -12,11 +12,11 @@ import { } from "~/utils/playwright"; import { NOTIFICATIONS_URL, + SETTINGS_PAGE, tournamentAdminPage, tournamentBracketsPage, tournamentMatchPage, tournamentPage, - tournamentRegisterPage, userResultsPage, } from "~/utils/urls"; @@ -825,7 +825,7 @@ test.describe("Tournament bracket", () => { await expect(page.locator('[data-match-id="1"]')).toBeVisible(); }); - test("tournament no screen toggle works", async ({ page }) => { + test("user no screen setting affects tournament match", async ({ page }) => { const tournamentId = 4; await seed(page); @@ -833,22 +833,25 @@ test.describe("Tournament bracket", () => { await navigate({ page, - url: tournamentRegisterPage(tournamentId), + url: SETTINGS_PAGE, }); - await page.getByTestId("no-screen-checkbox").click(); - await page.getByTestId("save-team-button").click(); + await page.getByTestId("UPDATE_NO_SCREEN-switch").click(); + + await navigate({ + page, + url: tournamentBracketsPage({ tournamentId }), + }); - await page.getByTestId("brackets-tab").click(); await page.getByTestId("finalize-bracket-button").click(); await page.getByTestId("confirm-finalize-bracket-button").click(); - await page.locator('[data-match-id="2"]').click(); - await expect(page.getByTestId("screen-allowed")).toBeVisible(); - await backToBracket(page); - await page.locator('[data-match-id="1"]').click(); await expect(page.getByTestId("screen-banned")).toBeVisible(); + + await backToBracket(page); + await page.locator('[data-match-id="2"]').click(); + await expect(page.getByTestId("screen-allowed")).toBeVisible(); }); test("hosts a 'play all' round robin stage", async ({ page }) => { diff --git a/locales/da/common.json b/locales/da/common.json index ecbc5fa7e..d69bf05d3 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "logindforsøg afbrudt", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -159,6 +162,8 @@ "forms.errors.noSearchMatches": "Ingen resultater fundet", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -293,6 +298,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/da/scrims.json b/locales/da/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/da/scrims.json +++ b/locales/da/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/da/tournament.json b/locales/da/tournament.json index 49ea04689..6c71a9ff9 100644 --- a/locales/da/tournament.json +++ b/locales/da/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Afregistrer", "pre.info.unregister.confirm": "Afregistrer fra turnering og slet holdinformationer?", "pre.info.noHost": "Mit hold foretrækker ikke at være vært for rummet", - "pre.info.noScreen": "Mit hold foretrækker at undgå Splattercolor Screen", "pre.roster.header": "Udfyld holdmedlemslisten", "pre.roster.footer": "Mindst {{atLeastCount}} holdmedlemmer kræves for at deltage. Der kan maks være {{maxCount}} på holdet", "pre.roster.addTrusted.header": "Tilføj personer, som du har spillet med", diff --git a/locales/de/common.json b/locales/de/common.json index 867aaf464..e979d87c5 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Einloggen abgebrochen", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -159,6 +162,8 @@ "forms.errors.noSearchMatches": "Keine Suchergebnisse", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -293,6 +298,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/de/scrims.json b/locales/de/scrims.json index 89d678d5b..60a298deb 100644 --- a/locales/de/scrims.json +++ b/locales/de/scrims.json @@ -1,37 +1,53 @@ { "tabs.owned": "Eigene", - "tabs.requests": "Anfragen", + "tabs.booked": "", "tabs.available": "Verfügbar", "now": "Jetzt", "noneAvailable": "Zur Zeit sind keine Scrims verfügbar. Schau später vorbei oder erstelle einen Post!", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "Scrim-Anfrage wird gesendet", - "table.headers.time": "Zeitpunkt", - "table.headers.team": "Team", - "table.headers.divs": "Divs", - "table.headers.status": "Status", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "Diese Anfrage hat aktuell eingeschränkte Sichtbarkeit aufgrund von Assoziationen.", - "pickup": "Pickup von {{username}}", - "status.booked": "Gebucht", - "status.pending": "Ausstehend", - "status.canceled": "", "actions.request": "Anfragen", - "deleteModal.title": "Scrim post löschen?", - "deleteModal.prevented": "Der Post muss von Ersteller ({{username}}) gelöscht werden.", - "cancelModal.title": "Anfrage zurückziehen?", + "actions.viewRequest": "", + "actions.contact": "", + "deleteModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "Kontaktieren", - "acceptModal.title": "Scrim-Anfrage von {{groupName}} akzeptieren & andere ablehnen (falls vorhanden)?", - "acceptModal.prevented": "Bitte den Ersteller des Scrim-Posts, diese Anfrage anzunehmen.", + "acceptModal.title": "", + "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "Neuen Scrim-Post erstellen", "forms.with.title": "Mit", "forms.with.explanation": "Du kannst nach Usern per Nutzername, Discord ID oder sendou.ink Profil-URL suchen.", "forms.with.user": "Nutzer {{nth}}", "forms.with.pick-up": "Pick-up", - "forms.when.title": "Wann", + "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "Text", "forms.visibility.title": "Sichtbarkeit", "forms.visibility.public": "Öffentlich", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "Leer lassen, wenn sich die Sichtbarkeit deines Posts nicht mit der Zeit ändern soll.", "forms.divs.minDiv.title": "Min div", "forms.divs.maxDiv.title": "Max div", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "Geplanter Scrim", "associations.title": "Assoziationen", "associations.explanation": "Erstelle eine Assoziation, um in einer kleineren Gruppe zu suchen (zum Beispiel mit regelmäßgien Übungsgegnern deines Teams oder deiner LUTI-Division).", @@ -55,5 +78,9 @@ "associations.forms.name.title": "Name", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/de/tournament.json b/locales/de/tournament.json index a17c3ae1a..a4ecadba1 100644 --- a/locales/de/tournament.json +++ b/locales/de/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Von Turnier abmelden", "pre.info.unregister.confirm": "Vom Turnier abmelden und Teaminfo löschen?", "pre.info.noHost": "Mein Team zieht es vor, keine Räume zu hosten", - "pre.info.noScreen": "Mein Team möchte Unsichtbarriere vermeiden", "pre.roster.header": "Roster füllen", "pre.roster.footer": "Mindestens {{atLeastCount}} Teammitglieder sind zum Spielen erforderlich. Maximale Rostergröße ist {{maxCount}}", "pre.roster.addTrusted.header": "Spieler hinzufügen, mit denen du gespielt hast", diff --git a/locales/en/common.json b/locales/en/common.json index 841d3d114..25050f710 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "New scrim scheduled at {{timeString}}", "notifications.title.SCRIM_CANCELED": "Scrim Canceled", "notifications.text.SCRIM_CANCELED": "The scrim at {{timeString}} was canceled", + "notifications.title.SCRIM_STARTING_SOON": "Scrim Starting Soon", + "notifications.text.SCRIM_STARTING_SOON": "Your scrim at {{timeString}} is starting soon", "notifications.title.COMMISSIONS_CLOSED": "Commissions Closed", "notifications.text.COMMISSIONS_CLOSED": "If your commissions are still open, please re-enable them", "auth.errors.aborted": "Login Aborted", @@ -123,6 +125,7 @@ "actions.enable": "Enable", "actions.disable": "Disable", "actions.accept": "Accept", + "actions.confirm": "Confirm", "actions.next": "Next", "actions.previous": "Previous", "actions.back": "Back", @@ -159,6 +162,8 @@ "forms.errors.noSearchMatches": "No matches found", "forms.userSearch.placeholder": "Search users by username, profile URL or Discord ID...", "forms.userSearch.noResults": "No users matching your search found", + "forms.tournamentSearch.placeholder": "Search tournaments by name...", + "forms.tournamentSearch.noResults": "No tournaments matching your search found", "forms.weaponSearch.placeholder": "Select a weapon", "forms.weaponSearch.search.placeholder": "Search weapons...", "forms.weaponSearch.quickSelect": "Recent", @@ -293,6 +298,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "Outside of your profile page, build abilities are sorted so that same abilities are next to each other. This setting allows you to see the abilities in the order they were authored everywhere.", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "Scrims: No adding to pickups by untrusted", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "By default anyone can add you. If you prefer to only allow users you explicitly set as trusted to add you to pickups, enable this setting.", + "settings.UPDATE_NO_SCREEN.label": "[Accessibility] Avoid Splattercolor Screen", + "settings.UPDATE_NO_SCREEN.bottomText": "Affects tournaments, scrims and SendouQ.", "settings.notifications.title": "Push notifications", "settings.notifications.description": "Receive push notifications to your device even if you don't currently have sendou.ink open.", "settings.notifications.disableInfo": "To disable push notifications check your browser settings", diff --git a/locales/en/scrims.json b/locales/en/scrims.json index 3f71ebb03..57570c99b 100644 --- a/locales/en/scrims.json +++ b/locales/en/scrims.json @@ -1,37 +1,53 @@ { "tabs.owned": "Owned", - "tabs.requests": "Requests", + "tabs.booked": "Booked", "tabs.available": "Available", "now": "Now", "noneAvailable": "No scrims available right now. Check back later or add your own!", + "noRequestsYet": "No requests yet", + "noOwnedPosts": "You don't have any open scrim posts currently", + "noBookedScrims": "No booked scrims", "requestModal.title": "Sending a scrim request", - "table.headers.time": "Time", - "table.headers.team": "Team", - "table.headers.divs": "Divs", - "table.headers.status": "Status", + "requestModal.message.label": "Message", + "requestModal.at.label": "Start time", + "requestModal.at.explanation": "Select a time within the post's time range", + "pickupBy": "Pickup by", + "filters.button": "Filters", + "filters.heading": "Scrim Filters", + "filters.weekdayTimes": "Weekday times", + "filters.weekdayStart": "Weekday start", + "filters.weekdayEnd": "Weekday end", + "filters.weekendTimes": "Weekend times", + "filters.weekendStart": "Weekend start", + "filters.weekendEnd": "Weekend end", + "filters.apply": "Apply", + "filters.applyAndDefault": "Apply & Set as Default", + "filters.showFiltered": "Show filtered ({{count}})", + "filters.hideFiltered": "Hide filtered ({{count}})", + "filters.showPendingRequests": "Show pending requests ({{count}})", + "filters.hidePendingRequests": "Hide pending requests ({{count}})", "limitedVisibility": "This post currently has limited visibility based on associations.", - "pickup": "{{username}}'s pickup", - "status.booked": "Booked", - "status.pending": "Pending", - "status.canceled": "Canceled", "actions.request": "Request", + "actions.viewRequest": "Request pending...", + "actions.contact": "Contact", "deleteModal.title": "Delete the scrim post?", - "deleteModal.prevented": "The post must be deleted by the owner ({{username}})", - "cancelModal.title": "Cancel the request?", + "cancelRequestModal.title": "Cancel your request?", "cancelModal.scrim.title": "Cancel the scrim?", "cancelModal.scrim.reasonLabel": "Reason for cancellation", "cancelModal.scrim.reasonExplanation": "Explain why you are cancelling the scrim. This will be visible to the other team.", - "actions.contact": "Contact", "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}}", "postModal.footer": "Post created {{time}}", "forms.title": "Creating a new scrim post", "forms.with.title": "With", "forms.with.explanation": "You can search for users with username, Discord ID or sendou.ink profile URL.", "forms.with.user": "User {{nth}}", "forms.with.pick-up": "Pick-up", - "forms.when.title": "When", + "forms.when.title": "Start", "forms.when.explanation": "Leave to default if you want to look for a scrim now", + "forms.rangeEnd.title": "Start time range end", + "forms.rangeEnd.explanation": "If set, allow requests for any time between start and end", "forms.text.title": "Text", "forms.visibility.title": "Visibility", "forms.visibility.public": "Public", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "Leave blank if you don't want the visibility of your post to change over time", "forms.divs.minDiv.title": "Min div", "forms.divs.maxDiv.title": "Max div", + "forms.maps.title": "Maps", + "forms.maps.noPreference": "No preference", + "forms.maps.szOnly": "SZ only", + "forms.maps.rankedOnly": "Ranked modes only", + "forms.maps.allModes": "All modes", + "forms.maps.tournament": "Tournament...", + "forms.mapsTournament.title": "Tournament", "page.scheduledScrim": "Scheduled scrim", "associations.title": "Associations", "associations.explanation": "Create an association to look in a smaller group (for example make one with your team's regular practice opponents or LUTI division).", @@ -55,5 +78,9 @@ "associations.forms.name.title": "Name", "forms.managedByAnyone.title": "Anyone can manage", "forms.managedByAnyone.explanation": "If enabled, all users in this post can accept requests and delete the post, not just the owner.", - "alert.canceled": "This scrim was canceled by {{user}}. Reason: {{reason}}" + "alert.canceled": "This scrim was canceled by {{user}}. Reason: {{reason}}", + "maps.header": "Maps", + "screenBan.header": "Screen", + "screenBan.warning": "Ask before playing Splattercolor Screen", + "screenBan.allowed": "Nobody in this scrim has indicated they want to avoid Splattercolor Screen" } diff --git a/locales/en/tournament.json b/locales/en/tournament.json index 154cc8911..675d9932e 100644 --- a/locales/en/tournament.json +++ b/locales/en/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Unregister", "pre.info.unregister.confirm": "Unregister from the tournament and delete team info?", "pre.info.noHost": "My team prefers not to host rooms", - "pre.info.noScreen": "My team prefers to avoid Splattercolor Screen", "pre.roster.header": "Fill roster", "pre.roster.footer": "At least {{atLeastCount}} members are required to participate. Max roster size is {{maxCount}}", "pre.roster.addTrusted.header": "Add people you have played with", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index aebce41fa..1bceffe54 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Ingreso cancelado", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "No se encuentran partidos", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -295,6 +300,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/es-ES/scrims.json b/locales/es-ES/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/es-ES/scrims.json +++ b/locales/es-ES/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/es-ES/tournament.json b/locales/es-ES/tournament.json index 84061eb7c..1c10d6727 100644 --- a/locales/es-ES/tournament.json +++ b/locales/es-ES/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Cancelar registro", "pre.info.unregister.confirm": "¿Cancelar registro del torneo y borrar info del equipo?", "pre.info.noHost": "Mi equipo prefiere no ser a cargo de salas", - "pre.info.noScreen": "Mi equipo prefiere evitar Muro marmoleado", "pre.roster.header": "Llenar equipo", "pre.roster.footer": "Se requieren al menos {{atLeastCount}} miembros para participar. Cantidad máxima son {{maxCount}}", "pre.roster.addTrusted.header": "Agregar personas con quienes has jugado", diff --git a/locales/es-US/common.json b/locales/es-US/common.json index 6e97f305f..929847b61 100644 --- a/locales/es-US/common.json +++ b/locales/es-US/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Ingreso cancelado", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "No se encuentran partidos", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -295,6 +300,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/es-US/scrims.json b/locales/es-US/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/es-US/scrims.json +++ b/locales/es-US/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/es-US/tournament.json b/locales/es-US/tournament.json index 5544ed27f..ca2f1bb0f 100644 --- a/locales/es-US/tournament.json +++ b/locales/es-US/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Cancelar registro", "pre.info.unregister.confirm": "¿Cancelar registro del torneo y borrar info del equipo?", "pre.info.noHost": "Mi equipo prefiere no ser a cargo de salas", - "pre.info.noScreen": "Mi equipo prefiere evitar Muro marmoleado", "pre.roster.header": "Llenar equipo", "pre.roster.footer": "Se requieren al menos {{atLeastCount}} miembros para participar. Cantidad máxima son {{maxCount}}", "pre.roster.addTrusted.header": "Agregar personas con quienes has jugado", diff --git a/locales/fr-CA/common.json b/locales/fr-CA/common.json index b35dd5d35..aa94d1c0a 100644 --- a/locales/fr-CA/common.json +++ b/locales/fr-CA/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Connexion abandonnée", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "Pas de correspondance trouvée", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -295,6 +300,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/fr-CA/scrims.json b/locales/fr-CA/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/fr-CA/scrims.json +++ b/locales/fr-CA/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/fr-CA/tournament.json b/locales/fr-CA/tournament.json index 7e6c5b778..5d4ffe951 100644 --- a/locales/fr-CA/tournament.json +++ b/locales/fr-CA/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Se désinscrire", "pre.info.unregister.confirm": "Se désinscrire du tournoi et effacer les info de l'équipe ?", "pre.info.noHost": "Mon équipe préfère ne par héberger", - "pre.info.noScreen": "", "pre.roster.header": "Remplir la liste", "pre.roster.footer": "Au moins {{atLeastCount}} membres sont requis pour participer. La taille maximum est de {{maxCount}}", "pre.roster.addTrusted.header": "Ajoutez des personnes avec lesquelles vous avez joué", diff --git a/locales/fr-EU/common.json b/locales/fr-EU/common.json index 561e7e18e..f90b2529f 100644 --- a/locales/fr-EU/common.json +++ b/locales/fr-EU/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "Nouveau scrim programmé à {{timeString}}", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Connexion abandonnée", @@ -123,6 +125,7 @@ "actions.enable": "Activer", "actions.disable": "Désactiver", "actions.accept": "Accepter", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "Pas de correspondance trouvée", "forms.userSearch.placeholder": "Cherchez les utilisateur par leur pseudo, l'URL de leur profil ou l'ID discord...", "forms.userSearch.noResults": "Aucun utilisateur trouvé", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -295,6 +300,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "En dehors de votre profil, les bonus des sets sont triées de manière à ce que les mêmes bonus soient côte à côte. Ce paramètre vous permet de voir les bonus dans l'ordre dans lequel elles ont été créées partout.", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "Scrims: Les ajouts aux pickups peuvent êtres effectuer uniquement par des personnes de confiance", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "Par défaut, n'importe qui peut vous ajouter. Si vous préférez autoriser uniquement les utilisateurs que vous avez explicitement définis comme fiables à vous ajouter aux pickups, activez ce paramètre.", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "Notifications push", "settings.notifications.description": "Recevoir une Receive notification push sur votre appareil quand vous n'êtes pas dessus.", "settings.notifications.disableInfo": "Regarder les paramètre de votre navigateur pour désactiver les notification push.", diff --git a/locales/fr-EU/scrims.json b/locales/fr-EU/scrims.json index d46834de1..8d6609e7a 100644 --- a/locales/fr-EU/scrims.json +++ b/locales/fr-EU/scrims.json @@ -1,37 +1,53 @@ { "tabs.owned": "Possédé", - "tabs.requests": "Demandes", + "tabs.booked": "", "tabs.available": "Disponible", "now": "Maintenant", "noneAvailable": "Aucun scrim disponible pour le moment. Revenez plus tard ou ajoutez le vôtre!", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "Envoi d'une demande de scrim", - "table.headers.time": "Heure", - "table.headers.team": "Team", - "table.headers.divs": "Divs", - "table.headers.status": "Statut", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "Cet article a actuellement une visibilité limitée en fonction des associations.", - "pickup": "Pickup de {{username}}", - "status.booked": "Réservé", - "status.pending": "En attente", - "status.canceled": "", "actions.request": "Demande", - "deleteModal.title": "Supprimer le post?", - "deleteModal.prevented": "Le post doit être supprimée par le propriétaire ({{username}})", - "cancelModal.title": "Annuler la demande ?", + "actions.viewRequest": "", + "actions.contact": "", + "deleteModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "Contact", - "acceptModal.title": "Accepter la demande de scrim de {{groupName}} et rejeter les autres (s'il y en a)?", - "acceptModal.prevented": "Demandez à la personne qui a posté le scrim d'accepter cette demande", + "acceptModal.title": "", + "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "Créer un nouveau post", "forms.with.title": "Avec", "forms.with.explanation": "Vous pouvez rechercher des utilisateurs avec leur pseudo, l'ID Discord ou l'URL de leur profil sendou.ink.", "forms.with.user": "Utilisateur {{nth}}", "forms.with.pick-up": "Pick-up", - "forms.when.title": "Quand", + "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "Texte", "forms.visibility.title": "Visibilité", "forms.visibility.public": "Public", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "Laissez vide si vous ne souhaitez pas que la visibilité de votre post change au fil du temps", "forms.divs.minDiv.title": "Min div", "forms.divs.maxDiv.title": "Max div", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "Scrim programmé", "associations.title": "Association", "associations.explanation": "Créez une ''association'' pour regarder dans un groupe plus petit (par exemple, créez une association avec les adversaires habituels de votre équipe ou avec la division LUTI).", @@ -55,5 +78,9 @@ "associations.forms.name.title": "Nom", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/fr-EU/tournament.json b/locales/fr-EU/tournament.json index 557c5d1fd..42f66f212 100644 --- a/locales/fr-EU/tournament.json +++ b/locales/fr-EU/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Se désinscrire", "pre.info.unregister.confirm": "Se désinscrire du tournoi et effacer les info de l'équipe ?", "pre.info.noHost": "Mon équipe préfère ne par héberger", - "pre.info.noScreen": "Ma team préfère éviter le Screen", "pre.roster.header": "Remplir la liste", "pre.roster.footer": "Au moins {{atLeastCount}} membres sont requis pour participer. La taille maximum est de {{maxCount}}", "pre.roster.addTrusted.header": "Ajoutez des personnes avec lesquelles vous avez joué", diff --git a/locales/he/common.json b/locales/he/common.json index c14105f88..de7d4fa48 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "הכניסה בוטלה", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -159,6 +162,8 @@ "forms.errors.noSearchMatches": "לא נמצאה התאמה", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -294,6 +299,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/he/scrims.json b/locales/he/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/he/scrims.json +++ b/locales/he/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/he/tournament.json b/locales/he/tournament.json index 65755f9d2..6440704cd 100644 --- a/locales/he/tournament.json +++ b/locales/he/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "ביטול הרשמה", "pre.info.unregister.confirm": "לבטל את הרישום לטורניר ולמחוק את פרטי הצוות?", "pre.info.noHost": "הצוות שלי מעדיף לא לארח חדרים", - "pre.info.noScreen": "", "pre.roster.header": "מלא צוות", "pre.roster.footer": "לפחות {{atLeastCount}} חברי צוות נדרשים כדי להשתתף. גודל הצוות המרבי הוא {{maxCount}}", "pre.roster.addTrusted.header": "הוסיפו אנשים ששיחקתם איתם", diff --git a/locales/it/common.json b/locales/it/common.json index 4e05c0d07..6a7f6fea7 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Accesso cancellato", @@ -123,6 +125,7 @@ "actions.enable": "Attiva", "actions.disable": "Disattiva", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "Nessun risultato trovato.", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -295,6 +300,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "Aldilà della tua pagina profilo, le build delle abilità sono ordinate in modo tale da rendere vicine le abilità uguali. Quest'impostazione ti permette di vedere le abilità come intese dall'autore ovunque.", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "Notifiche push", "settings.notifications.description": "Ricevi notifiche push sul tuo dispositivo anche se non hai attualmente sendou.ink aperto.", "settings.notifications.disableInfo": "Per disattivare le notifiche push, controllare le impostazioni del browser", diff --git a/locales/it/scrims.json b/locales/it/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/it/scrims.json +++ b/locales/it/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/it/tournament.json b/locales/it/tournament.json index 157e83797..aebe51b5e 100644 --- a/locales/it/tournament.json +++ b/locales/it/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Disiscriviti", "pre.info.unregister.confirm": "Disiscriversi dal torneo e cancellare le info del team?", "pre.info.noHost": "Il mio team preferisce non hostare le stanze", - "pre.info.noScreen": "Il mio team preferisce evitare la Cortina ingannevole", "pre.roster.header": "Riempi roster", "pre.roster.footer": "Sono necessari almeno {{atLeastCount}} membri per partecipare. La dimensione massima del roster è {{maxCount}}", "pre.roster.addTrusted.header": "Aggiungi persone con cui hai giocato", diff --git a/locales/ja/common.json b/locales/ja/common.json index 43f122110..d336efa4c 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "ログインを中断しました", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -157,6 +160,8 @@ "forms.errors.noSearchMatches": "検索結果がみつかりませんでした", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -289,6 +294,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/ja/scrims.json b/locales/ja/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/ja/scrims.json +++ b/locales/ja/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/ja/tournament.json b/locales/ja/tournament.json index 38f653172..5bf71c687 100644 --- a/locales/ja/tournament.json +++ b/locales/ja/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "登録解除", "pre.info.unregister.confirm": "トーナメント登録をキャンセルして、チーム情報を削除しますか?", "pre.info.noHost": "部屋作成はできれば遠慮したい", - "pre.info.noScreen": "スミナガシートはできるだけ使ってほしくない", "pre.roster.header": "参加プレイヤーを登録", "pre.roster.footer": "少なくとも {{atLeastCount}} 人の参加が必要です。最大メンバー数は {{maxCount}} です。", "pre.roster.addTrusted.header": "一緒にプレイしたことがあるプレイヤーを追加する", diff --git a/locales/ko/common.json b/locales/ko/common.json index 26757ecc6..0230e1b39 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "로그인 중단됨", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -157,6 +160,8 @@ "forms.errors.noSearchMatches": "검색 결과 없음", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -289,6 +294,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/ko/scrims.json b/locales/ko/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/ko/scrims.json +++ b/locales/ko/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/ko/tournament.json b/locales/ko/tournament.json index fe803c63c..cd98348b8 100644 --- a/locales/ko/tournament.json +++ b/locales/ko/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "", "pre.info.unregister.confirm": "", "pre.info.noHost": "", - "pre.info.noScreen": "", "pre.roster.header": "", "pre.roster.footer": "", "pre.roster.addTrusted.header": "", diff --git a/locales/nl/common.json b/locales/nl/common.json index d7f611f8b..94b398eb1 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -159,6 +162,8 @@ "forms.errors.noSearchMatches": "", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -293,6 +298,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/nl/scrims.json b/locales/nl/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/nl/scrims.json +++ b/locales/nl/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/nl/tournament.json b/locales/nl/tournament.json index 50f44195d..2285db729 100644 --- a/locales/nl/tournament.json +++ b/locales/nl/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "", "pre.info.unregister.confirm": "", "pre.info.noHost": "", - "pre.info.noScreen": "", "pre.roster.header": "", "pre.roster.footer": "", "pre.roster.addTrusted.header": "", diff --git a/locales/pl/common.json b/locales/pl/common.json index 6f947c6bf..3eb315203 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Logowanie przerwane", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "Nie znaleziono meczy", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -296,6 +301,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/pl/scrims.json b/locales/pl/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/pl/scrims.json +++ b/locales/pl/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/pl/tournament.json b/locales/pl/tournament.json index 0ca02e730..801437e63 100644 --- a/locales/pl/tournament.json +++ b/locales/pl/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "", "pre.info.unregister.confirm": "", "pre.info.noHost": "", - "pre.info.noScreen": "", "pre.roster.header": "", "pre.roster.footer": "", "pre.roster.addTrusted.header": "", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 09fb6780e..ea38076bf 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Login Abortado", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "Nenhum resultado encontrado", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -295,6 +300,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/pt-BR/scrims.json b/locales/pt-BR/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/pt-BR/scrims.json +++ b/locales/pt-BR/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/pt-BR/tournament.json b/locales/pt-BR/tournament.json index a4d525bb5..837be682e 100644 --- a/locales/pt-BR/tournament.json +++ b/locales/pt-BR/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Descadastrar", "pre.info.unregister.confirm": "Descadastrar do torneio e excluir info do time?", "pre.info.noHost": "Meu time prefere não hospedar salas", - "pre.info.noScreen": "Meu time prefere evitar a Splattercolor Screen", "pre.roster.header": "Preencher lista", "pre.roster.footer": "Pelo menos {{atLeastCount}} membros são necessários para participar. O número máximo da lista de participantes é de {{maxCount}}", "pre.roster.addTrusted.header": "Adicionar pessoas com que você jogou", diff --git a/locales/ru/common.json b/locales/ru/common.json index 612496bd1..b16529d5e 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "Новый скрим запланирован на {{timeString}}", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "Вход отменён", @@ -123,6 +125,7 @@ "actions.enable": "Включить", "actions.disable": "Выключить", "actions.accept": "Подтвердить", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -160,6 +163,8 @@ "forms.errors.noSearchMatches": "Совпадения не найдены", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -296,6 +301,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "За исключением вашего профиля, свойства в сборках отсортированы так, что одинаковые свойства находятся друг за другом. Эта настройка позволит вам видеть свойства в сборках в порядке, который задал автор.", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "Скримы: Не добавляться в пик-апы недоверенными лицами", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "По умолчанию вас может добавить в команду кто угодно. Включите эту настройку, если вы хотите, чтобы вас могли добавлять только доверенные лица.", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "Push-уведомления", "settings.notifications.description": "Получайте push-уведомления на ваше устройство даже если sendou.ink на нём не открыт.", "settings.notifications.disableInfo": "Чтобы отключить push-уведомления проверьте настройки вашего браузера", diff --git a/locales/ru/scrims.json b/locales/ru/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/ru/scrims.json +++ b/locales/ru/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/ru/tournament.json b/locales/ru/tournament.json index 68599329e..f83eb8f20 100644 --- a/locales/ru/tournament.json +++ b/locales/ru/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "Отменить регистрацию", "pre.info.unregister.confirm": "Отменить регистрацию и удалить информацию о команде?", "pre.info.noHost": "Моя команда предпочитает не организовывать игры", - "pre.info.noScreen": "Моя команда предпочитает избегать Заливной Барьер", "pre.roster.header": "Заполните состав", "pre.roster.footer": "Необходимый минимум игроков для данного турнира: {{atLeastCount}}. Максимальное количество игроков в составе: {{maxCount}}", "pre.roster.addTrusted.header": "Добавить людей, с которыми вы играли", diff --git a/locales/zh/common.json b/locales/zh/common.json index 404784505..57347d72f 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -78,6 +78,8 @@ "notifications.text.SCRIM_SCHEDULED": "", "notifications.title.SCRIM_CANCELED": "", "notifications.text.SCRIM_CANCELED": "", + "notifications.title.SCRIM_STARTING_SOON": "", + "notifications.text.SCRIM_STARTING_SOON": "", "notifications.title.COMMISSIONS_CLOSED": "", "notifications.text.COMMISSIONS_CLOSED": "", "auth.errors.aborted": "登录中止", @@ -123,6 +125,7 @@ "actions.enable": "", "actions.disable": "", "actions.accept": "", + "actions.confirm": "", "actions.next": "", "actions.previous": "", "actions.back": "", @@ -157,6 +160,8 @@ "forms.errors.noSearchMatches": "没有找到相关结果", "forms.userSearch.placeholder": "", "forms.userSearch.noResults": "", + "forms.tournamentSearch.placeholder": "", + "forms.tournamentSearch.noResults": "", "forms.weaponSearch.placeholder": "", "forms.weaponSearch.search.placeholder": "", "forms.weaponSearch.quickSelect": "", @@ -289,6 +294,8 @@ "settings.UPDATE_DISABLE_BUILD_ABILITY_SORTING.bottomText": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.label": "", "settings.DISALLOW_SCRIM_PICKUPS_FROM_UNTRUSTED.bottomText": "", + "settings.UPDATE_NO_SCREEN.label": "", + "settings.UPDATE_NO_SCREEN.bottomText": "", "settings.notifications.title": "", "settings.notifications.description": "", "settings.notifications.disableInfo": "", diff --git a/locales/zh/scrims.json b/locales/zh/scrims.json index 6ccd76089..0321fddcf 100644 --- a/locales/zh/scrims.json +++ b/locales/zh/scrims.json @@ -1,29 +1,43 @@ { "tabs.owned": "", - "tabs.requests": "", + "tabs.booked": "", "tabs.available": "", "now": "", "noneAvailable": "", + "noRequestsYet": "", + "noOwnedPosts": "", + "noBookedScrims": "", "requestModal.title": "", - "table.headers.time": "", - "table.headers.team": "", - "table.headers.divs": "", - "table.headers.status": "", + "requestModal.message.label": "", + "requestModal.at.label": "", + "requestModal.at.explanation": "", + "pickupBy": "", + "filters.button": "", + "filters.heading": "", + "filters.weekdayTimes": "", + "filters.weekdayStart": "", + "filters.weekdayEnd": "", + "filters.weekendTimes": "", + "filters.weekendStart": "", + "filters.weekendEnd": "", + "filters.apply": "", + "filters.applyAndDefault": "", + "filters.showFiltered": "", + "filters.hideFiltered": "", + "filters.showPendingRequests": "", + "filters.hidePendingRequests": "", "limitedVisibility": "", - "pickup": "", - "status.booked": "", - "status.pending": "", - "status.canceled": "", "actions.request": "", + "actions.viewRequest": "", + "actions.contact": "", "deleteModal.title": "", - "deleteModal.prevented": "", - "cancelModal.title": "", + "cancelRequestModal.title": "", "cancelModal.scrim.title": "", "cancelModal.scrim.reasonLabel": "", "cancelModal.scrim.reasonExplanation": "", - "actions.contact": "", "acceptModal.title": "", "acceptModal.prevented": "", + "acceptModal.confirmFor": "", "postModal.footer": "", "forms.title": "", "forms.with.title": "", @@ -32,6 +46,8 @@ "forms.with.pick-up": "", "forms.when.title": "", "forms.when.explanation": "", + "forms.rangeEnd.title": "", + "forms.rangeEnd.explanation": "", "forms.text.title": "", "forms.visibility.title": "", "forms.visibility.public": "", @@ -40,6 +56,13 @@ "forms.notFoundVisibility.explanation": "", "forms.divs.minDiv.title": "", "forms.divs.maxDiv.title": "", + "forms.maps.title": "", + "forms.maps.noPreference": "", + "forms.maps.szOnly": "", + "forms.maps.rankedOnly": "", + "forms.maps.allModes": "", + "forms.maps.tournament": "", + "forms.mapsTournament.title": "", "page.scheduledScrim": "", "associations.title": "", "associations.explanation": "", @@ -55,5 +78,9 @@ "associations.forms.name.title": "", "forms.managedByAnyone.title": "", "forms.managedByAnyone.explanation": "", - "alert.canceled": "" + "alert.canceled": "", + "maps.header": "", + "screenBan.header": "", + "screenBan.warning": "", + "screenBan.allowed": "" } diff --git a/locales/zh/tournament.json b/locales/zh/tournament.json index 13f29bd5f..4086cae6e 100644 --- a/locales/zh/tournament.json +++ b/locales/zh/tournament.json @@ -25,7 +25,6 @@ "pre.info.unregister": "取消报名", "pre.info.unregister.confirm": "取消报名并删除队伍信息?", "pre.info.noHost": "我的队伍倾向于不当房主", - "pre.info.noScreen": "我的队伍不希望使用浮墨幕墙", "pre.roster.header": "填写阵容", "pre.roster.footer": "需要至少 {{atLeastCount}} 名成员,成员数量上限为 {{maxCount}}", "pre.roster.addTrusted.header": "添加与您游玩过的玩家", diff --git a/migrations/101-scrim-request-message.js b/migrations/101-scrim-request-message.js new file mode 100644 index 000000000..0ace5b9d8 --- /dev/null +++ b/migrations/101-scrim-request-message.js @@ -0,0 +1,23 @@ +export function up(db) { + db.transaction(() => { + db.prepare( + /* sql */ `alter table "ScrimPostRequest" add "message" text`, + ).run(); + db.prepare( + /* sql */ `alter table "ScrimPost" add "rangeEnd" integer`, + ).run(); + db.prepare( + /* sql */ `alter table "ScrimPostRequest" add "at" integer`, + ).run(); + db.prepare(/* sql */ `alter table "ScrimPost" add "maps" text`).run(); + db.prepare( + /* sql */ `alter table "ScrimPost" add "mapsTournamentId" integer references "Tournament"("id") on delete cascade`, + ).run(); + db.prepare( + /* sql */ `create index "scrim_post_maps_tournament_id" on "ScrimPost"("mapsTournamentId")`, + ).run(); + db.prepare( + /* sql */ `alter table "TournamentTeam" drop column "noScreen"`, + ).run(); + })(); +}