From e957713d1eca8a85533ec712c5fd340cffe4c0a7 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 30 May 2026 07:02:31 +0300 Subject: [PATCH] Initial --- AGENTS.md | 1 + .../tournament-bracket/core/tests/mocks-li.ts | 1 + .../core/tests/mocks-sos.ts | 1 + .../tournament/TournamentRepository.server.ts | 10 + .../tournament/components/FactCard.module.css | 59 +++ .../tournament/components/FactCard.tsx | 41 ++ .../components/RegistrationActions.tsx | 38 ++ .../components/TournamentHeader.module.css | 92 +++++ .../components/TournamentHeader.tsx | 210 ++++++++++ .../components/TournamentNav.module.css | 149 +++++++ .../tournament/components/TournamentNav.tsx | 351 +++++++++++++++++ .../tournament/core/Tournament.test.ts | 102 +++++ app/features/tournament/core/Tournament.ts | 78 ++++ .../tournament/loaders/to.$id.info.server.ts | 24 ++ .../tournament/routes/to.$id.index.ts | 4 +- .../tournament/routes/to.$id.info.module.css | 33 ++ .../tournament/routes/to.$id.info.tsx | 117 ++++++ .../tournament/routes/to.$id.register.tsx | 372 ++---------------- .../tournament/routes/to.$id.rules.tsx | 77 ++++ app/features/tournament/routes/to.$id.tsx | 96 +---- app/features/tournament/tournament.module.css | 2 +- app/routes.ts | 2 + app/utils/urls.ts | 4 + e2e/org.spec.ts | 4 +- e2e/tournament.spec.ts | 8 +- locales/da/tournament.json | 36 +- locales/de/tournament.json | 36 +- locales/en/tournament.json | 36 +- locales/es-ES/tournament.json | 36 +- locales/es-US/tournament.json | 36 +- locales/fr-CA/tournament.json | 36 +- locales/fr-EU/tournament.json | 36 +- locales/he/tournament.json | 36 +- locales/it/tournament.json | 36 +- locales/ja/tournament.json | 36 +- locales/ko/tournament.json | 36 +- locales/nl/tournament.json | 36 +- locales/pl/tournament.json | 36 +- locales/pt-BR/tournament.json | 36 +- locales/ru/tournament.json | 36 +- locales/zh/tournament.json | 36 +- 41 files changed, 1829 insertions(+), 623 deletions(-) create mode 100644 app/features/tournament/components/FactCard.module.css create mode 100644 app/features/tournament/components/FactCard.tsx create mode 100644 app/features/tournament/components/RegistrationActions.tsx create mode 100644 app/features/tournament/components/TournamentHeader.module.css create mode 100644 app/features/tournament/components/TournamentHeader.tsx create mode 100644 app/features/tournament/components/TournamentNav.module.css create mode 100644 app/features/tournament/components/TournamentNav.tsx create mode 100644 app/features/tournament/core/Tournament.test.ts create mode 100644 app/features/tournament/core/Tournament.ts create mode 100644 app/features/tournament/loaders/to.$id.info.server.ts create mode 100644 app/features/tournament/routes/to.$id.info.module.css create mode 100644 app/features/tournament/routes/to.$id.info.tsx create mode 100644 app/features/tournament/routes/to.$id.rules.tsx diff --git a/AGENTS.md b/AGENTS.md index 26086a904..40b995c87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,7 @@ - before adding a new translation, check that one doesn't already exist you can reuse (particularly in the common.json) - add only English translation and use `pnpm run i18n:sync` to initialize other jsons with empty string ready for translators - when using namespace e.g. `const { t } = useTranslation("settings"]);` it needs to be defined in the `handle` for that route e.g. `export const handle: SendouRouteHandle = { i18n: ["settings"], ... }`. Certain namespaces are always included and you don't have to worry about those: "common", "forms", "game-misc", "weapons", "front", "friends" +- if changing translation key names make sure to port over any already translated values for non-english languages if the english language is unchanged ## Commit messages diff --git a/app/features/tournament-bracket/core/tests/mocks-li.ts b/app/features/tournament-bracket/core/tests/mocks-li.ts index 9fbbbbe56..fb9e1dfb2 100644 --- a/app/features/tournament-bracket/core/tests/mocks-li.ts +++ b/app/features/tournament-bracket/core/tests/mocks-li.ts @@ -6926,6 +6926,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ name: "Inkling Performance Labs", slug: "inkling-performance-labs", logoUrl: "fZrToLQrkqV3UZkdgwp0Q-1722263644749.webp", + series: [], members: [ { userId: 405, diff --git a/app/features/tournament-bracket/core/tests/mocks-sos.ts b/app/features/tournament-bracket/core/tests/mocks-sos.ts index 8004c0919..b96591e97 100644 --- a/app/features/tournament-bracket/core/tests/mocks-sos.ts +++ b/app/features/tournament-bracket/core/tests/mocks-sos.ts @@ -2029,6 +2029,7 @@ export const SWIM_OR_SINK_167 = ( name: "Inkling Performance Labs", slug: "inkling-performance-labs", logoUrl: "fZrToLQrkqV3UZkdgwp0Q-1722263644749.webp", + series: [], members: [ { userId: 405, diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts index f713b3084..772bb97a4 100644 --- a/app/features/tournament/TournamentRepository.server.ts +++ b/app/features/tournament/TournamentRepository.server.ts @@ -93,6 +93,16 @@ export async function findById(id: number) { "TournamentOrganization.id", ), ).as("members"), + jsonArrayFrom( + innerEb + .selectFrom("TournamentOrganizationSeries") + .select("TournamentOrganizationSeries.name") + .whereRef( + "TournamentOrganizationSeries.organizationId", + "=", + "TournamentOrganization.id", + ), + ).as("series"), ]) .whereRef( "TournamentOrganization.id", diff --git a/app/features/tournament/components/FactCard.module.css b/app/features/tournament/components/FactCard.module.css new file mode 100644 index 000000000..8bfd77213 --- /dev/null +++ b/app/features/tournament/components/FactCard.module.css @@ -0,0 +1,59 @@ +.wrapper { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: stretch; + gap: var(--s-8); + width: fit-content; + max-width: 100%; + margin-inline: auto; +} + +.column { + display: flex; + flex-direction: column; + gap: var(--s-3); +} + +.divider { + min-width: 2px; + background-color: var(--color-border-high); + border-radius: var(--radius-full); + flex-shrink: 0; +} + +.card { + display: grid; + grid-template-columns: minmax(0, 6rem) minmax(0, max-content); + align-items: center; + gap: var(--s-3); +} + +.label { + font-size: var(--font-xs); + font-weight: var(--weight-semi); + color: var(--color-text-high); + text-transform: uppercase; +} + +.value { + font-size: var(--font-sm); + font-weight: var(--weight-semi); + color: var(--color-text); + min-width: 0; + display: flex; + align-items: center; + gap: var(--s-2); + flex-wrap: wrap; +} + +/** xxx: divider gone and horizontal stack at the same time */ +@media (max-width: 480px) { + .wrapper { + grid-template-columns: auto; + gap: var(--s-3); + } + + .divider { + display: none; + } +} diff --git a/app/features/tournament/components/FactCard.tsx b/app/features/tournament/components/FactCard.tsx new file mode 100644 index 000000000..f92895adf --- /dev/null +++ b/app/features/tournament/components/FactCard.tsx @@ -0,0 +1,41 @@ +import type * as React from "react"; +import styles from "./FactCard.module.css"; + +export interface FactCardItem { + label: string; + value: React.ReactNode; +} + +export function FactCardGrid({ facts }: { facts: FactCardItem[] }) { + const leftFacts = facts.filter((_, i) => i % 2 === 0); + const rightFacts = facts.filter((_, i) => i % 2 === 1); + + return ( +
+
+ {leftFacts.map((fact) => ( + + ))} +
+ {rightFacts.length > 0 ? ( + <> + + ); +} + +function Card({ label, value }: FactCardItem) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/app/features/tournament/components/RegistrationActions.tsx b/app/features/tournament/components/RegistrationActions.tsx new file mode 100644 index 000000000..7daa93bdd --- /dev/null +++ b/app/features/tournament/components/RegistrationActions.tsx @@ -0,0 +1,38 @@ +import { ClipboardCheck, UserPlus } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { LinkButton } from "~/components/elements/Button"; +import type { Tournament } from "~/features/tournament-bracket/core/Tournament"; +import { tournamentRegisterPage, tournamentSubsPage } from "~/utils/urls"; + +export function RegistrationActions({ + tournament, +}: { + tournament: Tournament; +}) { + const { t } = useTranslation(["tournament"]); + + if (!tournament.registrationOpen) return null; + + return ( +
+ } + testId="register-cta" + > + {t("tournament:registerNow")} + + {tournament.lfgEnabled ? ( + } + > + {t("tournament:findTeam")} + + ) : null} +
+ ); +} diff --git a/app/features/tournament/components/TournamentHeader.module.css b/app/features/tournament/components/TournamentHeader.module.css new file mode 100644 index 000000000..2713e6ddb --- /dev/null +++ b/app/features/tournament/components/TournamentHeader.module.css @@ -0,0 +1,92 @@ +.header { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--s-4); + text-align: center; + container-type: inline-size; +} + +.identity { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--s-3); +} + +.titleBlock { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--s-2); +} + +@container (min-width: 448px) { + .identity { + flex-direction: row; + gap: var(--s-6); + } +} + +.logo { + border-radius: var(--radius-avatar); +} + +.nameBlock { + display: flex; + flex-direction: column; + align-items: stretch; + gap: var(--s-1); + width: max-content; + max-width: 100%; +} + +.name { + font-size: var(--font-xl); + font-weight: var(--weight-bold); + margin: 0; + text-wrap: balance; + text-align: center; + line-height: 0.9; +} + +.subtext { + display: flex; + align-items: center; + gap: var(--s-2); + font-size: var(--font-sm); + font-weight: var(--weight-semi); + color: var(--color-text-high); + + &::before, + &::after { + content: ""; + flex: 1; + border-bottom: 2px solid var(--color-text-high); + } +} + +.organizer { + display: inline-flex; + align-items: center; + gap: var(--s-2); + color: var(--color-text); + font-size: var(--font-sm); + font-weight: var(--weight-semi); +} + +.dates { + display: flex; + flex-direction: column; + gap: var(--s-1); + font-size: var(--font-sm); + font-weight: var(--weight-semi); + color: var(--color-text-high); +} + +.actions { + display: flex; + gap: var(--s-2); + align-items: center; + justify-content: center; +} diff --git a/app/features/tournament/components/TournamentHeader.tsx b/app/features/tournament/components/TournamentHeader.tsx new file mode 100644 index 000000000..877277ee7 --- /dev/null +++ b/app/features/tournament/components/TournamentHeader.tsx @@ -0,0 +1,210 @@ +import { Bookmark, BookmarkCheck, Share2 } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { Link, useFetcher } from "react-router"; +import * as R from "remeda"; +import { Avatar } from "~/components/Avatar"; +import { CopyToClipboardPopover } from "~/components/CopyToClipboardPopover"; +import { LinkButton, SendouButton } from "~/components/elements/Button"; +import { DiscordIcon } from "~/components/icons/Discord"; +import { LocaleTime } from "~/components/LocaleTime"; +import { useUser } from "~/features/auth/core/user"; +import type { Tournament } from "~/features/tournament-bracket/core/Tournament"; +import { databaseTimestampToDate } from "~/utils/dates"; +import { + SENDOU_INK_BASE_URL, + tournamentOrganizationPage, + tournamentPage, + userPage, +} from "~/utils/urls"; +import { splitTournamentName } from "../core/Tournament"; +import styles from "./TournamentHeader.module.css"; + +export function TournamentHeader({ tournament }: { tournament: Tournament }) { + const { name, subtext } = splitTournamentName( + tournament.ctx.name, + tournament.ctx.organization?.series ?? [], + ); + + const startTimes = R.uniqueBy( + [ + tournament.ctx.startTime, + ...tournament.ctx.settings.bracketProgression + .filter((b) => b.startTime) + .map((b) => databaseTimestampToDate(b.startTime!)), + ], + (date) => date.getTime(), + ); + + // xxx: for dates use the popover version + return ( +
+
+ +
+
+

{name}

+ {subtext ?
{subtext}
: null} +
+ +
+
+
+ {startTimes.map((date) => ( + + ))} +
+
+ ); +} + +export function TournamentHeaderActions({ + tournament, + isSaved, +}: { + tournament: Tournament; + isSaved: boolean; +}) { + return ( +
+ + {tournament.ctx.discordUrl ? ( + } + aria-label="Discord" + /> + ) : null} + +
+ ); +} + +function SaveTournamentButton({ + tournament, + isSaved, +}: { + tournament: Tournament; + isSaved: boolean; +}) { + const { t } = useTranslation(["common"]); + const user = useUser(); + const fetcher = useFetcher(); + + const teamMemberOf = tournament.teamMemberOfByUser(user); + if (!user || tournament.hasStarted || teamMemberOf) return null; + + const pending = fetcher.formData?.get("_action"); + const displayedSaved = + pending === "SAVE_TOURNAMENT" + ? true + : pending === "UNSAVE_TOURNAMENT" + ? false + : isSaved; + + return ( + + + : } + aria-label={ + displayedSaved ? t("common:actions.unsave") : t("common:actions.save") + } + /> + + ); +} + +function OrganizerLink({ tournament }: { tournament: Tournament }) { + if (tournament.ctx.organization) { + return ( + + + {tournament.ctx.organization.name} + + ); + } + + return ( + + + {tournament.ctx.author.username} + + ); +} + +function ShareTournamentButton({ tournament }: { tournament: Tournament }) { + const { t } = useTranslation(["common"]); + const url = `${SENDOU_INK_BASE_URL}${tournamentPage(tournament.ctx.id)}`; + + const handleShare = () => { + navigator.share({ url }); + }; + + if ( + typeof navigator !== "undefined" && + typeof navigator.share === "function" + ) { + return ( + } + onPress={handleShare} + aria-label={t("common:actions.share")} + /> + ); + } + + return ( + } + aria-label={t("common:actions.share")} + /> + } + /> + ); +} diff --git a/app/features/tournament/components/TournamentNav.module.css b/app/features/tournament/components/TournamentNav.module.css new file mode 100644 index 000000000..5267abec9 --- /dev/null +++ b/app/features/tournament/components/TournamentNav.module.css @@ -0,0 +1,149 @@ +.nav { + display: flex; + align-items: center; + gap: var(--s-3); + padding: var(--s-2) 0; + margin-block-end: var(--s-4); + min-width: 0; +} + +.identity { + display: flex; + align-items: center; + gap: var(--s-2); + color: var(--color-text); + text-decoration: none; + min-width: 0; + flex-shrink: 0; +} + +.identityText { + display: flex; + flex-direction: column; + gap: var(--s-0-5); + min-width: 0; +} + +.identityName { + font-size: var(--font-sm); + font-weight: var(--weight-bold); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 14ch; +} + +.identitySubtext { + display: flex; + align-items: center; + gap: var(--s-1); + font-size: var(--font-2xs); + font-weight: var(--weight-semi); + color: var(--color-text-high); + + &::before, + &::after { + content: ""; + flex: 1; + border-bottom: 1.5px solid var(--color-text-high); + } +} + +.separator { + flex-shrink: 0; + width: 2px; + height: 28px; + background-color: var(--color-border); +} + +.itemsWrapper { + flex: 1; + min-width: 0; + overflow: hidden; +} + +.items { + list-style: none; + margin: 0; + padding: 0; + display: flex; + gap: 4px; + flex-wrap: nowrap; + white-space: nowrap; +} + +.itemSlot[data-hidden="true"] { + visibility: hidden; + pointer-events: none; +} + +.link { + display: inline-flex; + align-items: center; + gap: var(--s-1-5); + padding: var(--s-1) var(--s-2-5); + border-radius: var(--radius-field); + color: var(--color-text); + font-size: var(--font-xs); + font-weight: var(--weight-semi); + text-decoration: none; + cursor: pointer; + + &:hover { + background-color: var(--color-bg-high); + } +} + +.linkActive { + color: var(--color-text-accent); + background-color: var(--color-bg-high); +} + +.icon { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.icon > svg { + width: 16px; + height: 16px; +} + +.label { + white-space: nowrap; +} + +.hamburger { + flex-shrink: 0; +} + +.overflowList { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 12rem; +} + +.overflowLink { + display: flex; + align-items: center; + gap: var(--s-2); + padding: var(--s-2) var(--s-3); + border-radius: var(--radius-field); + color: var(--color-text); + font-size: var(--font-sm); + font-weight: var(--weight-semi); + text-decoration: none; + + &:hover { + background-color: var(--color-bg-high); + } +} + +.overflowLink.linkActive { + color: var(--color-text-accent); +} diff --git a/app/features/tournament/components/TournamentNav.tsx b/app/features/tournament/components/TournamentNav.tsx new file mode 100644 index 000000000..e0a7ae883 --- /dev/null +++ b/app/features/tournament/components/TournamentNav.tsx @@ -0,0 +1,351 @@ +import clsx from "clsx"; +import { + ClipboardCheck, + LayoutGrid, + ListOrdered, + Medal, + Menu, + ScrollText, + Settings, + Trophy, + Tv, + UserPlus, + Users, +} from "lucide-react"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { NavLink } from "react-router"; +import { Avatar } from "~/components/Avatar"; +import { SendouButton } from "~/components/elements/Button"; +import { SendouPopover } from "~/components/elements/Popover"; +import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-controls"; +import { useUser } from "~/features/auth/core/user"; +import type { Tournament } from "~/features/tournament-bracket/core/Tournament"; +import { useIsomorphicLayoutEffect } from "~/hooks/useIsomorphicLayoutEffect"; +import { + tournamentDivisionsPage, + tournamentInfoPage, + tournamentRulesPage, +} from "~/utils/urls"; +import { splitTournamentName } from "../core/Tournament"; +import styles from "./TournamentNav.module.css"; + +type NavItemKey = + | "register" + | "brackets" + | "teams" + | "divisions" + | "streams" + | "results" + | "rules" + | "lfg" + | "seeds" + | "admin"; + +interface NavItem { + key: NavItemKey; + label: string; + to: string; + icon: React.ReactNode; + end?: boolean; + testId?: string; +} + +const PRIORITY_ORDER: NavItemKey[] = [ + "register", + "lfg", + "brackets", + "teams", + "divisions", + "streams", + "results", + "rules", + "seeds", + "admin", +]; + +// xxx: icons shrinking +// xxx: sticky for desktop +// xxx: close popover when changing page + +export function TournamentNav({ + tournament, + hasChildTournaments, +}: { + tournament: Tournament; + hasChildTournaments: boolean; +}) { + const { t } = useTranslation(["tournament"]); + const navItems = useNavItems({ tournament, hasChildTournaments }); + const { visibleCount, containerRef, measureRef } = useNavOverflow( + navItems.length, + ); + + const overflowItems = navItems.slice(visibleCount); + + const { name, subtext } = splitTournamentName( + tournament.ctx.name, + tournament.ctx.organization?.series ?? [], + ); + + const homeHref = tournament.isLeagueDivision + ? tournamentInfoPage(tournament.ctx.parentTournamentId!) + : tournamentInfoPage(tournament.ctx.id); + + return ( + + ); +} + +function useNavItems({ + tournament, + hasChildTournaments, +}: { + tournament: Tournament; + hasChildTournaments: boolean; +}): NavItem[] { + const { t } = useTranslation(["tournament"]); + const user = useUser(); + + const items: Partial> = {}; + + if (tournament.registrationOpen) { + items.register = { + key: "register", + label: t("tournament:nav.register"), + to: "register", + icon: , + testId: "register-tab", + }; + } + + const showBrackets = tournament.hasStarted && !tournament.isLeagueSignup; + if (showBrackets) { + items.brackets = { + key: "brackets", + label: t("tournament:nav.brackets"), + to: "brackets", + icon: , + testId: "brackets-tab", + }; + } + + const showTeams = !(tournament.isLeagueSignup && hasChildTournaments); + if (showTeams) { + items.teams = { + key: "teams", + label: t("tournament:nav.teams", { + count: tournament.ctx.teams.length, + }), + to: "teams", + icon: , + end: false, + testId: "teams-tab", + }; + } + + if (tournament.isLeagueSignup || tournament.isLeagueDivision) { + items.divisions = { + key: "divisions", + label: t("tournament:nav.divisions"), + to: tournamentDivisionsPage( + tournament.ctx.parentTournamentId ?? tournament.ctx.id, + ), + icon: , + }; + } + + if (tournament.hasStarted && !tournament.everyBracketOver) { + items.streams = { + key: "streams", + label: t("tournament:nav.streams", { + count: tournament.streams.length, + }), + to: "streams", + icon: , + }; + } + + if (tournament.hasStarted) { + items.results = { + key: "results", + label: t("tournament:nav.results"), + to: "results", + icon: , + testId: "results-tab", + }; + } + + if (tournament.ctx.rules) { + items.rules = { + key: "rules", + label: t("tournament:nav.rules"), + to: tournamentRulesPage(tournament.ctx.id), + icon: , + }; + } + + const showLfg = + !tournament.isInvitational && + !tournament.everyBracketOver && + !(tournament.isLeagueSignup && !tournament.registrationOpen) && + tournament.lfgEnabled; + if (showLfg) { + items.lfg = { + key: "lfg", + label: tournament.registrationOpen + ? t("tournament:nav.looking") + : t("tournament:nav.subs"), + to: "looking", + icon: , + }; + } + + const showSeeds = + tournament.isOrganizer(user) && + !tournament.hasStarted && + !tournament.isLeagueSignup; + if (showSeeds) { + items.seeds = { + key: "seeds", + label: t("tournament:nav.seeds"), + to: "seeds", + icon: , + }; + } + + const showAdmin = + tournament.isOrganizer(user) && + (!tournament.ctx.isFinalized || DANGEROUS_CAN_ACCESS_DEV_CONTROLS); + if (showAdmin) { + items.admin = { + key: "admin", + label: t("tournament:nav.admin"), + to: "admin", + icon: , + testId: "admin-tab", + }; + } + + return PRIORITY_ORDER.flatMap((key) => (items[key] ? [items[key]!] : [])); +} + +function NavItemLink({ + item, + overflow = false, +}: { + item: NavItem; + overflow?: boolean; +}) { + return ( + + clsx(overflow ? styles.overflowLink : styles.link, { + [styles.linkActive]: isActive, + }) + } + data-testid={item.testId} + > + + {item.label} + + ); +} + +const ITEM_GAP = 4; + +function useNavOverflow(totalItems: number) { + const containerRef = React.useRef(null); + const measureRef = React.useRef(null); + const [visibleCount, setVisibleCount] = React.useState(totalItems); + + useIsomorphicLayoutEffect(() => { + const container = containerRef.current; + const list = measureRef.current; + if (!container || !list) return; + + const slots = Array.from(list.children) as HTMLElement[]; + + const computeVisible = () => { + const containerWidth = container.clientWidth; + + let used = 0; + let count = 0; + for (const slot of slots) { + const width = slot.scrollWidth + (count === 0 ? 0 : ITEM_GAP); + if (used + width <= containerWidth) { + used += width; + count++; + } else { + break; + } + } + setVisibleCount(count); + }; + + computeVisible(); + + const observer = new ResizeObserver(() => computeVisible()); + observer.observe(container); + for (const slot of slots) { + observer.observe(slot); + } + + return () => observer.disconnect(); + }, [totalItems]); + + return { visibleCount, containerRef, measureRef }; +} diff --git a/app/features/tournament/core/Tournament.test.ts b/app/features/tournament/core/Tournament.test.ts new file mode 100644 index 000000000..7ae5e12e7 --- /dev/null +++ b/app/features/tournament/core/Tournament.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import { bracketProgressionLabel, splitTournamentName } from "./Tournament"; + +describe("splitTournamentName", () => { + const series = [{ name: "In The Zone" }, { name: "Low Ink" }]; + + it("splits the trailing number subtext after the series name", () => { + expect(splitTournamentName("In The Zone 54", series)).toEqual({ + name: "In The Zone", + subtext: "54", + }); + }); + + it("splits a non-numeric subtext after the series name", () => { + expect(splitTournamentName("Low Ink May 2026", series)).toEqual({ + name: "Low Ink", + subtext: "May 2026", + }); + }); + + it("matches the series name case-insensitively", () => { + expect(splitTournamentName("in the zone 54", series)).toEqual({ + name: "In The Zone", + subtext: "54", + }); + }); + + it("strips separators between the series name and the subtext", () => { + expect(splitTournamentName("In The Zone - 54", series)).toEqual({ + name: "In The Zone", + subtext: "54", + }); + }); + + it("trims trailing whitespace after the subtext", () => { + expect(splitTournamentName("In The Zone 54 ", series)).toEqual({ + name: "In The Zone", + subtext: "54", + }); + }); + + it("returns name only when the name does not start with a series name", () => { + expect(splitTournamentName("Picnic Weekly", series)).toEqual({ + name: "Picnic Weekly", + }); + }); + + it("returns name only when the name equals the series name", () => { + expect(splitTournamentName("In The Zone", series)).toEqual({ + name: "In The Zone", + }); + }); + + it("returns name only when there are no series", () => { + expect(splitTournamentName("In The Zone 54", [])).toEqual({ + name: "In The Zone 54", + }); + }); + + it("prefers the longest matching series name", () => { + expect( + splitTournamentName("In The Zone Masters 5", [ + { name: "In The Zone" }, + { name: "In The Zone Masters" }, + ]), + ).toEqual({ + name: "In The Zone Masters", + subtext: "5", + }); + }); +}); + +describe("bracketProgressionLabel", () => { + it("returns the short code for a single stage", () => { + expect(bracketProgressionLabel([{ type: "single_elimination" }])).toBe( + "SE", + ); + }); + + it("joins stages with an arrow", () => { + expect( + bracketProgressionLabel([ + { type: "round_robin" }, + { type: "single_elimination" }, + ]), + ).toBe("RR → SE"); + }); + + it("collapses consecutive duplicate stages", () => { + expect( + bracketProgressionLabel([ + { type: "single_elimination" }, + { type: "single_elimination" }, + { type: "double_elimination" }, + ]), + ).toBe("SE → DE"); + }); + + it("returns empty string for empty progression", () => { + expect(bracketProgressionLabel([])).toBe(""); + }); +}); diff --git a/app/features/tournament/core/Tournament.ts b/app/features/tournament/core/Tournament.ts new file mode 100644 index 000000000..684b68290 --- /dev/null +++ b/app/features/tournament/core/Tournament.ts @@ -0,0 +1,78 @@ +import type { TournamentStage } from "~/db/tables"; +import type { ParsedBracket } from "../../tournament-bracket/core/Progression"; + +const LEADING_SEPARATOR_REGEX = /^[\s_-]+/; + +/** + * Splits a tournament name into its series name and a trailing "subtext" + * (e.g. an edition number like `"54"` or a date like `"May 2026"`) based on the + * names of the organization's tournament series. + * + * The longest series name that the tournament name starts with (case-insensitive) + * is treated as the base name and whatever follows it becomes the subtext. If the + * tournament name does not start with any of the series names, the whole name is + * returned with no subtext. + * + * @example + * // series: [{ name: "In The Zone" }] + * splitTournamentName("In The Zone 54", series) // { name: "In The Zone", subtext: "54" } + * splitTournamentName("In The Zone Winter", series) // { name: "In The Zone", subtext: "Winter" } + * splitTournamentName("Picnic Weekly", series) // { name: "Picnic Weekly" } + */ +export function splitTournamentName( + tournamentName: string, + series: Array<{ name: string }>, +): { name: string; subtext?: string } { + const trimmedName = tournamentName.trim(); + const nameLower = trimmedName.toLowerCase(); + + const matchingSeries = series + .filter((s) => nameLower.startsWith(s.name.toLowerCase())) + .sort((a, b) => b.name.length - a.name.length) + .at(0); + + if (!matchingSeries) return { name: trimmedName }; + + const subtext = trimmedName + .slice(matchingSeries.name.length) + .replace(LEADING_SEPARATOR_REGEX, "") + .trim(); + + if (!subtext) return { name: matchingSeries.name }; + + return { name: matchingSeries.name, subtext }; +} + +const STAGE_TYPE_TO_SHORT_CODE: Record = { + single_elimination: "SE", + double_elimination: "DE", + round_robin: "RR", + swiss: "SW", +}; + +/** + * Builds a compact arrow-separated label describing the bracket progression of a tournament, + * derived from `settings.bracketProgression`. + * + * Each stage type is rendered as a short code (`RR`, `SE`, `DE`, `SW`) and consecutive duplicates + * are collapsed so e.g. two single-elimination stages still render as a single `SE`. + * + * @example + * // [{type: "round_robin"}, {type: "single_elimination"}] + * bracketProgressionLabel(progression) // "RR → SE" + */ +export function bracketProgressionLabel( + progression: Pick[], +): string { + if (progression.length === 0) return ""; + + const codes: string[] = []; + for (const bracket of progression) { + const code = STAGE_TYPE_TO_SHORT_CODE[bracket.type]; + if (codes.at(-1) !== code) { + codes.push(code); + } + } + + return codes.join(" → "); +} diff --git a/app/features/tournament/loaders/to.$id.info.server.ts b/app/features/tournament/loaders/to.$id.info.server.ts new file mode 100644 index 000000000..097787021 --- /dev/null +++ b/app/features/tournament/loaders/to.$id.info.server.ts @@ -0,0 +1,24 @@ +import type { LoaderFunctionArgs } from "react-router"; +import { getUser } from "~/features/auth/core/user.server"; +import * as SavedCalendarEventRepository from "~/features/tournament/SavedCalendarEventRepository.server"; +import { parseParams } from "~/utils/remix.server"; +import { idObject } from "~/utils/zod"; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const user = getUser(); + const { id: tournamentId } = parseParams({ + params, + schema: idObject, + }); + + if (!user) { + return { isSaved: false }; + } + + return { + isSaved: await SavedCalendarEventRepository.isSaved({ + userId: user.id, + tournamentId, + }), + }; +}; diff --git a/app/features/tournament/routes/to.$id.index.ts b/app/features/tournament/routes/to.$id.index.ts index 4c5d2c1c0..61e10fd2d 100644 --- a/app/features/tournament/routes/to.$id.index.ts +++ b/app/features/tournament/routes/to.$id.index.ts @@ -3,7 +3,7 @@ import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tourn import { parseParams } from "~/utils/remix.server"; import { tournamentBracketsPage, - tournamentRegisterPage, + tournamentInfoPage, tournamentResultsPage, } from "~/utils/urls"; import { idObject } from "~/utils/zod"; @@ -20,7 +20,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { }); if (!tournament.hasStarted) { - return redirect(tournamentRegisterPage(tournamentId)); + return redirect(tournamentInfoPage(tournamentId)); } if (!tournament.ctx.isFinalized) { diff --git a/app/features/tournament/routes/to.$id.info.module.css b/app/features/tournament/routes/to.$id.info.module.css new file mode 100644 index 000000000..96d5f0b76 --- /dev/null +++ b/app/features/tournament/routes/to.$id.info.module.css @@ -0,0 +1,33 @@ +.description { + white-space: pre-wrap; + + & > :is(h1, h2, h3, h4, h5, h6) { + margin-block-end: var(--s-4); + } + + & > :is(h2, h3, h4, h5, h6) { + margin-block-start: var(--s-6); + } + + & > :first-child { + margin-block-start: 0; + } + + & > h1 { + font-size: var(--font-xl); + } + + & > :is(h2, h3, h4, h5, h6) { + font-size: var(--font-lg); + } + + & > :is(h3, h4, h5, h6) { + font-size: var(--font-md); + } +} + +.modes { + display: inline-flex; + gap: var(--s-1); + flex-wrap: wrap; +} diff --git a/app/features/tournament/routes/to.$id.info.tsx b/app/features/tournament/routes/to.$id.info.tsx new file mode 100644 index 000000000..d888137bc --- /dev/null +++ b/app/features/tournament/routes/to.$id.info.tsx @@ -0,0 +1,117 @@ +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { useLoaderData } from "react-router"; +import { ModeImage } from "~/components/Image"; +import { containerClassName } from "~/components/Main"; +import { Markdown } from "~/components/Markdown"; +import { TierPill } from "~/components/TierPill"; +import * as Seasons from "~/features/mmr/core/Seasons"; +import type { SendouRouteHandle } from "~/utils/remix.server"; +import { FactCardGrid, type FactCardItem } from "../components/FactCard"; +import { RegistrationActions } from "../components/RegistrationActions"; +import { + TournamentHeader, + TournamentHeaderActions, +} from "../components/TournamentHeader"; +import { bracketProgressionLabel } from "../core/Tournament"; +import { loader } from "../loaders/to.$id.info.server"; +import { useTournament } from "./to.$id"; +import styles from "./to.$id.info.module.css"; + +export { loader }; + +export const handle: SendouRouteHandle = { + i18n: ["tournament"], +}; + +// xxx: align round outlined buttons and fact card + +export default function TournamentInfoPage() { + const tournament = useTournament(); + const data = useLoaderData(); + const facts = useFacts(tournament); + + return ( +
+ +
+ + +
+ + {tournament.ctx.description ? ( +
+ {tournament.ctx.description} +
+ ) : null} +
+ ); +} + +function useFacts( + tournament: ReturnType, +): FactCardItem[] { + const { t } = useTranslation(["tournament"]); + + const teamSizeValue = + tournament.minMembersPerTeam === tournament.maxMembersPerTeam + ? `${tournament.minMembersPerTeam}` + : `${tournament.minMembersPerTeam}–${tournament.maxMembersPerTeam}`; + + const showsEstimatedTier = !tournament.ctx.tier && !tournament.hasStarted; + + const rankedSeason = Seasons.current(tournament.ctx.startTime); + + return [ + { + label: t("tournament:fact.format"), + value: `${tournament.minMembersPerTeam}v${tournament.minMembersPerTeam}`, + }, + { + label: t("tournament:fact.bracket"), + value: bracketProgressionLabel( + tournament.ctx.settings.bracketProgression, + ), + }, + { + label: t("tournament:fact.modes"), + value: ( +
+ {tournament.modesIncluded.map((mode) => ( + + ))} +
+ ), + }, + { + label: showsEstimatedTier + ? t("tournament:fact.tier.est") + : t("tournament:fact.tier"), + value: tournament.ctx.tier ? ( + + ) : showsEstimatedTier && tournament.ctx.tentativeTier ? ( + + ) : ( + "-" + ), + }, + { + label: t("tournament:fact.ranked"), + value: + tournament.ranked && rankedSeason + ? t("tournament:fact.ranked.yesWithSeason", { + season: rankedSeason.nth, + }) + : tournament.ranked + ? t("tournament:fact.ranked.yes") + : t("tournament:fact.ranked.no"), + }, + { + label: t("tournament:fact.teamSize"), + value: teamSizeValue, + }, + ]; +} diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index 7b772412d..436fa375e 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -1,45 +1,21 @@ import clsx from "clsx"; import Compressor from "compressorjs"; -import { - AlertCircle, - Bookmark, - BookmarkCheck, - Check, - Clock, - Share2, - Trash, - User, - X, -} from "lucide-react"; +import { AlertCircle, Check, Trash, X } from "lucide-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; -import { Form, Link, useFetcher, useLoaderData } from "react-router"; +import { Form, useFetcher, useLoaderData } from "react-router"; import { useCopyToClipboard } from "react-use"; import { Alert } from "~/components/Alert"; import { Avatar } from "~/components/Avatar"; -import { CopyToClipboardPopover } from "~/components/CopyToClipboardPopover"; import { Divider } from "~/components/Divider"; import { LinkButton, SendouButton } from "~/components/elements/Button"; import { SendouPopover } from "~/components/elements/Popover"; -import { - SendouTab, - SendouTabList, - SendouTabPanel, - SendouTabs, -} from "~/components/elements/Tabs"; import { FormWithConfirm } from "~/components/FormWithConfirm"; import { FriendCodePopover } from "~/components/FriendCodePopover"; -import { Image, ModeImage } from "~/components/Image"; import { Input } from "~/components/Input"; -import { DiscordIcon } from "~/components/icons/Discord"; import { Label } from "~/components/Label"; import { containerClassName } from "~/components/Main"; -import { MapPoolStages } from "~/components/MapPoolSelector"; -import { Markdown } from "~/components/Markdown"; -import { Section } from "~/components/Section"; import { SubmitButton } from "~/components/SubmitButton"; -import { TierPill } from "~/components/TierPill"; -import TimePopover from "~/components/TimePopover"; import { useUser } from "~/features/auth/core/user"; import { imgTypeToDimensions } from "~/features/img-upload/upload-constants"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; @@ -48,21 +24,14 @@ import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tour import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { useAutoRerender } from "~/hooks/useAutoRerender"; import { useHydrated } from "~/hooks/useHydrated"; -import { useSearchParamState } from "~/hooks/useSearchParamState"; -import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes"; +import { rankedModesShort } from "~/modules/in-game-lists/modes"; import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; import { LOG_IN_URL, - mapsPageWithMapPool, - navIconUrl, SENDOU_INK_BASE_URL, tournamentJoinPage, - tournamentOrganizationPage, - tournamentPage, - tournamentSubsPage, userEditProfilePage, - userPage, } from "~/utils/urls"; import { action } from "../actions/to.$id.register.server"; import type { TournamentRegisterPageLoader } from "../loaders/to.$id.register.server"; @@ -78,101 +47,6 @@ import { useTournament } from "./to.$id"; export { action, loader }; export default function TournamentRegisterPage() { - const isHydrated = useHydrated(); - const tournament = useTournament(); - - return ( -
-
- -
-
{tournament.ctx.name}
-
- {tournament.ctx.organization ? ( - - - {tournament.ctx.organization.name} - - ) : ( - - {" "} - {tournament.ctx.author.username} - - )} -
- {!tournament.isLeagueSignup ? ( -
-
- {" "} - {isHydrated ? ( - - ) : null} -
-
- ) : null} -
- {tournament.ranked ? ( -
- Ranked -
- ) : ( -
- Unranked -
- )} - {tournament.ctx.tier ? ( - - ) : tournament.ctx.tentativeTier && !tournament.hasStarted ? ( - - ) : null} -
- {tournament.modesIncluded.map((mode) => ( - - ))} -
-
-
-
- -
- ); -} - -const TABS = ["description", "rules", "register"] as const; -type RegisterPageTab = (typeof TABS)[number]; - -function TournamentRegisterInfoTabs() { const user = useUser(); const tournament = useTournament(); const { t } = useTranslation(["tournament"]); @@ -181,18 +55,6 @@ function TournamentRegisterInfoTabs() { const teamOwned = tournament.ownedTeamByUser(user); const isRegularMemberOfATeam = teamMemberOf && !teamOwned; - const defaultTab = (): RegisterPageTab => { - if (tournament.hasStarted || !teamOwned) return "description"; - - return "register"; - }; - const [tabKey, setTabKey] = useSearchParamState({ - defaultValue: defaultTab(), - name: "tab", - revive: (val) => - TABS.includes(val as RegisterPageTab) ? (val as RegisterPageTab) : null, - }); - const showAddIGNAlert = tournament.ctx.settings.requireInGameNames && !teamOwned && @@ -200,97 +62,26 @@ function TournamentRegisterInfoTabs() { !user?.inGameName; return ( -
- setTabKey(key as RegisterPageTab)} - > - - Description - {tournament.ctx.rules ? ( - Rules - ) : null} - {!tournament.hasStarted ? ( - - Register - - ) : null} - - - -
-
- {tournament.ctx.discordUrl ? ( -
- } - > - Join the Discord - -
- ) : null} - - +
+ {isRegularMemberOfATeam ? ( +
+ {t("tournament:pre.inATeam")} + +
+ ) : showAddIGNAlert ? ( +
+ +
+ This tournament requires you to have an in-game name set{" "} + + Edit profile +
- -
- {tournament.ctx.description ?? ""} -
- - -
- - - {tournament.ctx.rules ? ( - -
- {tournament.ctx.rules ?? ""} -
-
- ) : null} - - {!tournament.hasStarted ? ( - -
- {isRegularMemberOfATeam ? ( -
- {t("tournament:pre.inATeam")} - -
- ) : showAddIGNAlert ? ( -
- -
- This tournament requires you to have an in-game name set{" "} - - Edit profile - -
-
-
- ) : ( - - )} - {user && - !tournament.teamMemberOfByUser(user) && - tournament.canAddNewSubPost && - !showAddIGNAlert && - !tournament.hasStarted ? ( - - {t("tournament:pre.sub.prompt")} - - ) : null} -
-
- ) : null} - + +
+ ) : ( + + )}
); } @@ -653,7 +444,6 @@ function TeamInfo({ const formData = new FormData(ref.current!); if (uploadedAvatar) { - // replace with the compressed version formData.delete("img"); formData.append("img", uploadedAvatar, uploadedAvatar.name); } @@ -870,7 +660,6 @@ function TournamentLogoUpload({ width: logoDimensions.width, maxHeight: logoDimensions.height, maxWidth: logoDimensions.width, - // 0.5MB convertSize: 500_000, resize: "cover", success(result) { @@ -1308,120 +1097,3 @@ function MapPoolValidationStatusMessage({
); } - -function SaveTournamentButton() { - const { t } = useTranslation(["common"]); - const user = useUser(); - const tournament = useTournament(); - const data = useLoaderData(); - const fetcher = useFetcher(); - - const teamMemberOf = tournament.teamMemberOfByUser(user); - if (!user || tournament.hasStarted || teamMemberOf) return null; - - const isSaved = - fetcher.formData?.get("_action") === "SAVE_TOURNAMENT" - ? true - : fetcher.formData?.get("_action") === "UNSAVE_TOURNAMENT" - ? false - : (data?.isSaved ?? false); - - return ( - - - : } - > - {isSaved ? t("common:actions.unsave") : t("common:actions.save")} - - - ); -} - -function ShareTournamentButton() { - const { t } = useTranslation(["common"]); - const tournament = useTournament(); - - const url = `${SENDOU_INK_BASE_URL}${tournamentPage(tournament.ctx.id)}`; - - const handleShare = () => { - navigator.share({ url }); - }; - - if ( - typeof navigator !== "undefined" && - typeof navigator.share === "function" - ) { - return ( - } - onPress={handleShare} - > - {t("common:actions.share")} - - ); - } - - return ( - }> - {t("common:actions.share")} - - } - /> - ); -} - -function TOPickedMapPoolInfo() { - const { t } = useTranslation(["calendar"]); - const tournament = useTournament(); - - if (tournament.ctx.toSetMapPool.length === 0) return null; - - const mapPool = new MapPool(tournament.ctx.toSetMapPool); - - return ( -
-
- -
- - - {t("calendar:createMapList")} - -
-
-
- ); -} - -function TiebreakerMapPoolInfo() { - const { t } = useTranslation(["game-misc"]); - const tournament = useTournament(); - - if (tournament.ctx.tieBreakerMapPool.length === 0) return null; - - return ( -
- Tiebreaker map pool:{" "} - {tournament.ctx.tieBreakerMapPool - .sort((a, b) => modesShort.indexOf(a.mode) - modesShort.indexOf(b.mode)) - .map( - (map) => - `${t(`game-misc:MODE_SHORT_${map.mode}`)} ${t(`game-misc:STAGE_${map.stageId}`)}`, - ) - .join(", ")} -
- ); -} diff --git a/app/features/tournament/routes/to.$id.rules.tsx b/app/features/tournament/routes/to.$id.rules.tsx new file mode 100644 index 000000000..f9b02a381 --- /dev/null +++ b/app/features/tournament/routes/to.$id.rules.tsx @@ -0,0 +1,77 @@ +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { LinkButton } from "~/components/elements/Button"; +import { Image } from "~/components/Image"; +import { containerClassName } from "~/components/Main"; +import { MapPoolStages } from "~/components/MapPoolSelector"; +import { Markdown } from "~/components/Markdown"; +import { Section } from "~/components/Section"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; +import { modesShort } from "~/modules/in-game-lists/modes"; +import type { SendouRouteHandle } from "~/utils/remix.server"; +import { mapsPageWithMapPool, navIconUrl } from "~/utils/urls"; +import { useTournament } from "./to.$id"; +import styles from "./to.$id.info.module.css"; + +export const handle: SendouRouteHandle = { + i18n: ["tournament", "calendar", "game-misc"], +}; + +export default function TournamentRulesPage() { + const tournament = useTournament(); + + return ( +
+ {tournament.ctx.rules ? ( +
+ {tournament.ctx.rules} +
+ ) : null} + + +
+ ); +} + +function CounterPickMapPool() { + const { t } = useTranslation(["calendar"]); + const tournament = useTournament(); + + if (tournament.ctx.toSetMapPool.length === 0) return null; + + const mapPool = new MapPool(tournament.ctx.toSetMapPool); + + return ( +
+
+ +
+ + + {t("calendar:createMapList")} + +
+
+
+ ); +} + +function TiebreakerMapPool() { + const { t } = useTranslation(["game-misc"]); + const tournament = useTournament(); + + if (tournament.ctx.tieBreakerMapPool.length === 0) return null; + + return ( +
+ Tiebreaker map pool:{" "} + {tournament.ctx.tieBreakerMapPool + .sort((a, b) => modesShort.indexOf(a.mode) - modesShort.indexOf(b.mode)) + .map( + (map) => + `${t(`game-misc:MODE_SHORT_${map.mode}`)} ${t(`game-misc:STAGE_${map.stageId}`)}`, + ) + .join(", ")} +
+ ); +} diff --git a/app/features/tournament/routes/to.$id.tsx b/app/features/tournament/routes/to.$id.tsx index c7fbdbb0c..722f7e468 100644 --- a/app/features/tournament/routes/to.$id.tsx +++ b/app/features/tournament/routes/to.$id.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import { useTranslation } from "react-i18next"; import type { MetaFunction } from "react-router"; import { Outlet, @@ -9,20 +8,14 @@ import { } from "react-router"; import { Main } from "~/components/Main"; import { Placeholder } from "~/components/Placeholder"; -import { SubNav, SubNavLink } from "~/components/SubNav"; -import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-controls"; -import { useUser } from "~/features/auth/core/user"; import { useChatContext } from "~/features/chat/useChatContext"; import { Tournament } from "~/features/tournament-bracket/core/Tournament"; import { useHydrated } from "~/hooks/useHydrated"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { removeMarkdown } from "~/utils/strings"; -import { - tournamentDivisionsPage, - tournamentPage, - tournamentRegisterPage, -} from "~/utils/urls"; +import { tournamentPage } from "~/utils/urls"; import { metaTags } from "../../../utils/remix"; +import { TournamentNav } from "../components/TournamentNav"; import { loader, type TournamentLoaderData } from "../loaders/to.$id.server"; @@ -99,8 +92,6 @@ export default function TournamentLayoutShell() { } export function TournamentLayout() { - const { t } = useTranslation(["tournament"]); - const user = useUser(); const rawData = useLoaderData(); const data = React.useMemo( () => JSON.parse(rawData) as TournamentLoaderData, @@ -124,85 +115,10 @@ export function TournamentLayout() { } return (
- - - {tournament.hasStarted || tournament.isLeagueDivision - ? "Info" - : t("tournament:tabs.register")} - - {!tournament.isLeagueSignup ? ( - - {t("tournament:tabs.brackets")} - - ) : null} - {tournament.isLeagueSignup || tournament.isLeagueDivision ? ( - - Divisions - - ) : null} - {!(tournament.isLeagueSignup && data.hasChildTournaments) ? ( - - {t("tournament:tabs.teams", { - count: tournament.ctx.teams.length, - })} - - ) : null} - {!tournament.isInvitational && - !tournament.everyBracketOver && - !(tournament.isLeagueSignup && !tournament.registrationOpen) && - tournament.lfgEnabled ? ( - - {tournament.registrationOpen - ? t("tournament:tabs.looking") - : t("tournament:tabs.subs")} - - ) : null} - {tournament.hasStarted && !tournament.everyBracketOver ? ( - - {t("tournament:tabs.streams", { - count: tournament.streams.length, - })} - - ) : null} - {tournament.hasStarted ? ( - - {t("tournament:tabs.results")} - - ) : null} - {tournament.isOrganizer(user) && - !tournament.hasStarted && - !tournament.isLeagueSignup && ( - {t("tournament:tabs.seeds")} - )} - {tournament.isOrganizer(user) && - (!tournament.ctx.isFinalized || - DANGEROUS_CAN_ACCESS_DEV_CONTROLS) && ( - - {t("tournament:tabs.admin")} - - )} - + `/to/${tournamentId}/teams/${tournamentTeamId}`; +export const tournamentInfoPage = (tournamentId: number) => + `/to/${tournamentId}/info`; export const tournamentRegisterPage = (tournamentId: number) => `/to/${tournamentId}/register`; +export const tournamentRulesPage = (tournamentId: number) => + `/to/${tournamentId}/rules`; export const tournamentAdminPage = (tournamentId: number) => `/to/${tournamentId}/admin`; export const tournamentBracketsPage = ({ diff --git a/e2e/org.spec.ts b/e2e/org.spec.ts index 8bcd64630..80dd4918c 100644 --- a/e2e/org.spec.ts +++ b/e2e/org.spec.ts @@ -140,7 +140,7 @@ test.describe("Tournament Organization", () => { }); // Try to create a team - await page.getByRole("tab", { name: "Register" }).click(); + await page.getByTestId("register-cta").click(); // Fill in team details await page.getByLabel("Team name").fill("Banned Team"); @@ -164,7 +164,7 @@ test.describe("Tournament Organization", () => { page, url: tournamentPage(1), }); - await page.getByRole("tab", { name: "Register" }).click(); + await page.getByTestId("register-cta").click(); // Try to create a team again await expect(page.getByText(/Teams \(\d+\)/)).toBeVisible(); diff --git a/e2e/tournament.spec.ts b/e2e/tournament.spec.ts index e4498bed0..2ef435d77 100644 --- a/e2e/tournament.spec.ts +++ b/e2e/tournament.spec.ts @@ -4,6 +4,7 @@ import type { StageId } from "~/modules/in-game-lists/types"; import { tournamentBracketsPage, tournamentPage, + tournamentRegisterPage, tournamentTeamsPage, } from "~/utils/urls"; import { @@ -67,7 +68,7 @@ test.describe("Tournament", () => { url: tournamentPage(1), }); - await page.getByRole("tab", { name: "Register" }).click(); + await page.getByTestId("register-cta").click(); await page.getByLabel("Pick-up name").fill("Chimera"); await page.getByTestId("save-team-button").click(); @@ -106,7 +107,10 @@ test.describe("Tournament", () => { await isNotVisible(page.getByText("Chimera")); - await page.getByTestId("register-tab").click(); + await navigate({ + page, + url: tournamentRegisterPage(3), + }); await submit(page, "check-in-button"); await page.getByTestId("brackets-tab").click(); diff --git a/locales/da/tournament.json b/locales/da/tournament.json index 29c96da1c..9a299358c 100644 --- a/locales/da/tournament.json +++ b/locales/da/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Information", - "tabs.teams": "Hold ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Register", - "tabs.brackets": "Grupper", - "tabs.seeds": "Seedninger", - "tabs.results": "", - "tabs.streams": "Streams ({{count}})", - "tabs.subs": "Suppleanter", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Grupper", + "nav.register": "", + "nav.teams": "Hold ({{count}})", + "nav.streams": "Streams ({{count}})", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Suppleanter", + "nav.divisions": "", + "nav.seeds": "Seedninger", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Fuldfør disse trin for at spille", "pre.steps.name": "Holdnavn", "pre.steps.roster": "Holdmedlemmer", @@ -35,7 +50,6 @@ "pre.pool.header": "Vælg banepulje", "pre.pool.banned": "Bandlyst", "pre.pool.tiebreaker.short": "Tiebreaker", - "pre.sub.prompt": "Fandt du ikke et hold til denne begivenhed? Du kan skrive dig på listen af Suppleant.", "bracket.type.DE_WINNERS": "Runde", "bracket.type.DE_LOSERS": "Taber Runde", "bracket.type.SE": "Runde", diff --git a/locales/de/tournament.json b/locales/de/tournament.json index 2bd6f2a1a..c2e4fd4e0 100644 --- a/locales/de/tournament.json +++ b/locales/de/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Teams ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Registrieren", - "tabs.brackets": "Brackets", - "tabs.seeds": "Seeds", - "tabs.results": "Ergebnisse", - "tabs.streams": "Streams ({{count}})", - "tabs.subs": "Ersatzspieler", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Brackets", + "nav.register": "", + "nav.teams": "Teams ({{count}})", + "nav.streams": "Streams ({{count}})", + "nav.results": "Ergebnisse", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Ersatzspieler", + "nav.divisions": "", + "nav.seeds": "Seeds", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Folge diesen Schritten, um zu spielen", "pre.steps.name": "Teamname", "pre.steps.roster": "Volles Roster", @@ -35,7 +50,6 @@ "pre.pool.header": "Arenenpool wählen", "pre.pool.banned": "Gebannt", "pre.pool.tiebreaker.short": "Tiebreaker", - "pre.sub.prompt": "Kein Team für dieses Event im Sinn? Du kannst dich auch als Ersatzspieler eintragen.", "bracket.type.DE_WINNERS": "Sieger-Runde", "bracket.type.DE_LOSERS": "Verlierer-Runde", "bracket.type.SE": "Runde", diff --git a/locales/en/tournament.json b/locales/en/tournament.json index 102d47850..ceb5d3777 100644 --- a/locales/en/tournament.json +++ b/locales/en/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Teams ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Register", - "tabs.brackets": "Brackets", - "tabs.seeds": "Seeds", - "tabs.results": "Results", - "tabs.streams": "Streams ({{count}})", - "tabs.subs": "Subs", - "tabs.looking": "LFG", + "nav.label": "Tournament navigation", + "nav.moreItems": "More", + "nav.brackets": "Brackets", + "nav.register": "Register", + "nav.teams": "Teams ({{count}})", + "nav.streams": "Streams ({{count}})", + "nav.results": "Results", + "nav.rules": "Rules", + "nav.looking": "LFG", + "nav.subs": "Subs", + "nav.divisions": "Divisions", + "nav.seeds": "Seeds", + "nav.admin": "Admin", + "findTeam": "Find team", + "registerNow": "Register now", + "fact.format": "Format", + "fact.bracket": "Bracket", + "fact.modes": "Modes", + "fact.tier": "Tier", + "fact.tier.est": "Est. tier", + "fact.ranked": "Ranked", + "fact.ranked.yes": "Yes", + "fact.ranked.yesWithSeason": "Yes (S{{season}})", + "fact.ranked.no": "No", + "fact.teamSize": "Team size", "pre.steps.header": "Complete these steps to play", "pre.steps.name": "Team name", "pre.steps.roster": "Full roster", @@ -35,7 +50,6 @@ "pre.pool.header": "Pick map pool", "pre.pool.banned": "Banned", "pre.pool.tiebreaker.short": "Tiebreaker", - "pre.sub.prompt": "No team in mind for this event? You can also join the list of subs.", "bracket.type.DE_WINNERS": "Winners Round", "bracket.type.DE_LOSERS": "Losers Round", "bracket.type.SE": "Round", diff --git a/locales/es-ES/tournament.json b/locales/es-ES/tournament.json index b5c74a93b..05033df25 100644 --- a/locales/es-ES/tournament.json +++ b/locales/es-ES/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Equipos ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Registrar", - "tabs.brackets": "Cuadros de Torneo", - "tabs.seeds": "Listas de Equipos", - "tabs.results": "Resultados", - "tabs.streams": "Streams ({{count}})", - "tabs.subs": "Subs", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Cuadros de Torneo", + "nav.register": "", + "nav.teams": "Equipos ({{count}})", + "nav.streams": "Streams ({{count}})", + "nav.results": "Resultados", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Subs", + "nav.divisions": "", + "nav.seeds": "Listas de Equipos", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Completa estos pasos para jugar", "pre.steps.name": "Nombre de equipo", "pre.steps.roster": "Equipo lleno", @@ -35,7 +50,6 @@ "pre.pool.header": "Escojer grupo de mapas", "pre.pool.banned": "Prohibidos", "pre.pool.tiebreaker.short": "Desempate", - "pre.sub.prompt": "¿No tienes equipo en mente para este evento? También puedes unirte a la lista de substitutos.", "bracket.type.DE_WINNERS": "Cuadro de ganadores", "bracket.type.DE_LOSERS": "Cuadro de perdedores", "bracket.type.SE": "Cuadro", diff --git a/locales/es-US/tournament.json b/locales/es-US/tournament.json index 961ad72e5..0fd8d037e 100644 --- a/locales/es-US/tournament.json +++ b/locales/es-US/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Equipos ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Registrar", - "tabs.brackets": "Cuadros de Torneo", - "tabs.seeds": "Listas de Equipos", - "tabs.results": "", - "tabs.streams": "Streams ({{count}})", - "tabs.subs": "Subs", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Cuadros de Torneo", + "nav.register": "", + "nav.teams": "Equipos ({{count}})", + "nav.streams": "Streams ({{count}})", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Subs", + "nav.divisions": "", + "nav.seeds": "Listas de Equipos", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Completa estos pasos para jugar", "pre.steps.name": "Nombre de equipo", "pre.steps.roster": "Equipo lleno", @@ -35,7 +50,6 @@ "pre.pool.header": "Escojer grupo de mapas", "pre.pool.banned": "Prohibidos", "pre.pool.tiebreaker.short": "Desempate", - "pre.sub.prompt": "¿No tienes equipo en mente para este evento? También puedes unirte a la lista de substitutos.", "bracket.type.DE_WINNERS": "Cuadro de ganadores", "bracket.type.DE_LOSERS": "Cuadro de perdedores", "bracket.type.SE": "Cuadro", diff --git a/locales/fr-CA/tournament.json b/locales/fr-CA/tournament.json index 422a68bbb..9a01ae857 100644 --- a/locales/fr-CA/tournament.json +++ b/locales/fr-CA/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Informations", - "tabs.teams": "Équipes ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Registration", - "tabs.brackets": "Brackets", - "tabs.seeds": "Seeds", - "tabs.results": "", - "tabs.streams": "Diffusions ({{count}})", - "tabs.subs": "Remplaçants", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Brackets", + "nav.register": "", + "nav.teams": "Équipes ({{count}})", + "nav.streams": "Diffusions ({{count}})", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Remplaçants", + "nav.divisions": "", + "nav.seeds": "Seeds", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Complétez ces étapes pour jouer", "pre.steps.name": "Nom de l'équipe", "pre.steps.roster": "Participants", @@ -35,7 +50,6 @@ "pre.pool.header": "Sélection de stage", "pre.pool.banned": "", "pre.pool.tiebreaker.short": "", - "pre.sub.prompt": "Pas d'équipe en tête pour cet évenement ? Vous pouvez aussi rejoindre la liste des remplaçants.", "bracket.type.DE_WINNERS": "Manche des gagnants", "bracket.type.DE_LOSERS": "Manche des perdants", "bracket.type.SE": "Manche", diff --git a/locales/fr-EU/tournament.json b/locales/fr-EU/tournament.json index 0b82996ea..ebe244d07 100644 --- a/locales/fr-EU/tournament.json +++ b/locales/fr-EU/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Informations", - "tabs.teams": "Équipes ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Registration", - "tabs.brackets": "Brackets", - "tabs.seeds": "Seeds", - "tabs.results": "Resultats", - "tabs.streams": "Diffusions ({{count}})", - "tabs.subs": "Remplaçants", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Brackets", + "nav.register": "", + "nav.teams": "Équipes ({{count}})", + "nav.streams": "Diffusions ({{count}})", + "nav.results": "Resultats", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Remplaçants", + "nav.divisions": "", + "nav.seeds": "Seeds", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Complétez ces étapes pour jouer", "pre.steps.name": "Nom de l'équipe", "pre.steps.roster": "Participants", @@ -35,7 +50,6 @@ "pre.pool.header": "Sélection de stage", "pre.pool.banned": "Bannis", "pre.pool.tiebreaker.short": "Manche décisive", - "pre.sub.prompt": "Pas d'équipe en tête pour cet évenement ? Vous pouvez aussi rejoindre la liste des remplaçants.", "bracket.type.DE_WINNERS": "Manche des gagnants", "bracket.type.DE_LOSERS": "Manche des perdants", "bracket.type.SE": "Manche", diff --git a/locales/he/tournament.json b/locales/he/tournament.json index 54fb9ee6d..a7fd22d7b 100644 --- a/locales/he/tournament.json +++ b/locales/he/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "מידע", - "tabs.teams": "צוותים ({{count}})", - "tabs.admin": "מנהל", - "tabs.register": "הרשמה", - "tabs.brackets": "מערכים", - "tabs.seeds": "דירוג", - "tabs.results": "", - "tabs.streams": "שידורים חיים ({{count}})", - "tabs.subs": "מחליפים", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "מערכים", + "nav.register": "", + "nav.teams": "צוותים ({{count}})", + "nav.streams": "שידורים חיים ({{count}})", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "מחליפים", + "nav.divisions": "", + "nav.seeds": "דירוג", + "nav.admin": "מנהל", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "השלימו את השלבים הבאים כדי לשחק", "pre.steps.name": "שם קבוצה", "pre.steps.roster": "צוות מלא", @@ -35,7 +50,6 @@ "pre.pool.header": "בחרו מאגר מפות", "pre.pool.banned": "", "pre.pool.tiebreaker.short": "", - "pre.sub.prompt": "אין לכם קבוצה מראש לאירוע? אתם יכולים להצטרף לרשימת הממלאי מקום.", "bracket.type.DE_WINNERS": "סיבוב מנצחים", "bracket.type.DE_LOSERS": "סיבוב מפסידים", "bracket.type.SE": "סיבוב", diff --git a/locales/it/tournament.json b/locales/it/tournament.json index 103ee465e..4d14974aa 100644 --- a/locales/it/tournament.json +++ b/locales/it/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Squadre ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "Registra", - "tabs.brackets": "Bracket", - "tabs.seeds": "Seed", - "tabs.results": "Resultati", - "tabs.streams": "Stream ({{count}})", - "tabs.subs": "Sub", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Bracket", + "nav.register": "", + "nav.teams": "Squadre ({{count}})", + "nav.streams": "Stream ({{count}})", + "nav.results": "Resultati", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Sub", + "nav.divisions": "", + "nav.seeds": "Seed", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Completa questi passaggi per giocare", "pre.steps.name": "Nome team", "pre.steps.roster": "Roster completo", @@ -35,7 +50,6 @@ "pre.pool.header": "Scegli pool mappe", "pre.pool.banned": "Banneta", "pre.pool.tiebreaker.short": "Spareggio", - "pre.sub.prompt": "Non hai nessun team in mente per questo evento? Puoi anche unirti alla lista dei sub.", "bracket.type.DE_WINNERS": "Round Vincitori", "bracket.type.DE_LOSERS": "Round Perdenti", "bracket.type.SE": "Round", diff --git a/locales/ja/tournament.json b/locales/ja/tournament.json index c19244b52..7c720fb14 100644 --- a/locales/ja/tournament.json +++ b/locales/ja/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "情報", - "tabs.teams": "チーム ({{count}})", - "tabs.admin": "管理", - "tabs.register": "登録", - "tabs.brackets": "ブラケット", - "tabs.seeds": "シード", - "tabs.results": "結果", - "tabs.streams": "配信 ({{count}})", - "tabs.subs": "サブ", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "ブラケット", + "nav.register": "", + "nav.teams": "チーム ({{count}})", + "nav.streams": "配信 ({{count}})", + "nav.results": "結果", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "サブ", + "nav.divisions": "", + "nav.seeds": "シード", + "nav.admin": "管理", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "トーナメント参加のために下記を入力してください", "pre.steps.name": "チーム名", "pre.steps.roster": "全プレイヤー", @@ -35,7 +50,6 @@ "pre.pool.header": "マッププールを選択する", "pre.pool.banned": "禁止", "pre.pool.tiebreaker.short": "タイブレイカー", - "pre.sub.prompt": "このイベントで参加したいチームが見当たらない場合は、サブで参加することもできます。", "bracket.type.DE_WINNERS": "勝者ラウンド", "bracket.type.DE_LOSERS": "敗者ラウンド", "bracket.type.SE": "ラウンド", diff --git a/locales/ko/tournament.json b/locales/ko/tournament.json index 26b318cd2..e2b6dba34 100644 --- a/locales/ko/tournament.json +++ b/locales/ko/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "", - "tabs.teams": "", - "tabs.admin": "", - "tabs.register": "", - "tabs.brackets": "", - "tabs.seeds": "", - "tabs.results": "", - "tabs.streams": "", - "tabs.subs": "", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "", + "nav.register": "", + "nav.teams": "", + "nav.streams": "", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "", + "nav.divisions": "", + "nav.seeds": "", + "nav.admin": "", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "", "pre.steps.name": "", "pre.steps.roster": "", @@ -35,7 +50,6 @@ "pre.pool.header": "", "pre.pool.banned": "", "pre.pool.tiebreaker.short": "", - "pre.sub.prompt": "", "bracket.type.DE_WINNERS": "", "bracket.type.DE_LOSERS": "", "bracket.type.SE": "", diff --git a/locales/nl/tournament.json b/locales/nl/tournament.json index fef6f8f25..1d3667a1a 100644 --- a/locales/nl/tournament.json +++ b/locales/nl/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "", - "tabs.teams": "", - "tabs.admin": "", - "tabs.register": "", - "tabs.brackets": "", - "tabs.seeds": "", - "tabs.results": "", - "tabs.streams": "", - "tabs.subs": "", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "", + "nav.register": "", + "nav.teams": "", + "nav.streams": "", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "", + "nav.divisions": "", + "nav.seeds": "", + "nav.admin": "", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "", "pre.steps.name": "", "pre.steps.roster": "", @@ -35,7 +50,6 @@ "pre.pool.header": "", "pre.pool.banned": "", "pre.pool.tiebreaker.short": "", - "pre.sub.prompt": "", "bracket.type.DE_WINNERS": "", "bracket.type.DE_LOSERS": "", "bracket.type.SE": "", diff --git a/locales/pl/tournament.json b/locales/pl/tournament.json index 55d66aab8..f65f97f04 100644 --- a/locales/pl/tournament.json +++ b/locales/pl/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Drużyny ({{count}})", - "tabs.admin": "Admin", - "tabs.register": "", - "tabs.brackets": "", - "tabs.seeds": "", - "tabs.results": "", - "tabs.streams": "", - "tabs.subs": "", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "", + "nav.register": "", + "nav.teams": "Drużyny ({{count}})", + "nav.streams": "", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "", + "nav.divisions": "", + "nav.seeds": "", + "nav.admin": "Admin", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "", "pre.steps.name": "", "pre.steps.roster": "", @@ -35,7 +50,6 @@ "pre.pool.header": "", "pre.pool.banned": "", "pre.pool.tiebreaker.short": "", - "pre.sub.prompt": "", "bracket.type.DE_WINNERS": "Runda Zwycięzców", "bracket.type.DE_LOSERS": "Runda Przegranych", "bracket.type.SE": "Runda", diff --git a/locales/pt-BR/tournament.json b/locales/pt-BR/tournament.json index 9ca727998..ad2c86b77 100644 --- a/locales/pt-BR/tournament.json +++ b/locales/pt-BR/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Info", - "tabs.teams": "Times ({{count}})", - "tabs.admin": "Administrador", - "tabs.register": "Registrar", - "tabs.brackets": "Brackets", - "tabs.seeds": "Sementes", - "tabs.results": "", - "tabs.streams": "Transmissões ({{count}})", - "tabs.subs": "Inscritos", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Brackets", + "nav.register": "", + "nav.teams": "Times ({{count}})", + "nav.streams": "Transmissões ({{count}})", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Inscritos", + "nav.divisions": "", + "nav.seeds": "Sementes", + "nav.admin": "Administrador", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Complete esses passos para jogar", "pre.steps.name": "Nome do time", "pre.steps.roster": "Lista de membros completa", @@ -35,7 +50,6 @@ "pre.pool.header": "Escolher seleção de mapas", "pre.pool.banned": "Banido", "pre.pool.tiebreaker.short": "Desempate", - "pre.sub.prompt": "Não tem um time em mente para esse evento? Você também pode se juntar à lista de substitutos.", "bracket.type.DE_WINNERS": "Round dos Vencedores", "bracket.type.DE_LOSERS": "Round dos Perdedores", "bracket.type.SE": "Round", diff --git a/locales/ru/tournament.json b/locales/ru/tournament.json index 14dffb84d..001bd1c63 100644 --- a/locales/ru/tournament.json +++ b/locales/ru/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "Информация", - "tabs.teams": "Команды: ({{count}})", - "tabs.admin": "Администратор", - "tabs.register": "Регистрация", - "tabs.brackets": "Сетки", - "tabs.seeds": "Семена", - "tabs.results": "Результаты", - "tabs.streams": "Трансляции: ({{count}})", - "tabs.subs": "Запасные", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "Сетки", + "nav.register": "", + "nav.teams": "Команды: ({{count}})", + "nav.streams": "Трансляции: ({{count}})", + "nav.results": "Результаты", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "Запасные", + "nav.divisions": "", + "nav.seeds": "Семена", + "nav.admin": "Администратор", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "Завершите следующие шаги, чтобы закончить регистрацию", "pre.steps.name": "Имя команды", "pre.steps.roster": "Полный состав", @@ -35,7 +50,6 @@ "pre.pool.header": "Выберите пул арен", "pre.pool.banned": "Запрещено", "pre.pool.tiebreaker.short": "Тайбрейк", - "pre.sub.prompt": "Нет команды? Запишитесь в качестве запасного для данного турнира.", "bracket.type.DE_WINNERS": "Раунд победителей", "bracket.type.DE_LOSERS": "Раунд проигравших", "bracket.type.SE": "Раунд", diff --git a/locales/zh/tournament.json b/locales/zh/tournament.json index 4ce0d412d..b41b11527 100644 --- a/locales/zh/tournament.json +++ b/locales/zh/tournament.json @@ -1,14 +1,29 @@ { - "tabs.info": "比赛信息", - "tabs.teams": "参赛队伍 ({{count}})", - "tabs.admin": "管理", - "tabs.register": "报名", - "tabs.brackets": "对战表", - "tabs.seeds": "种子", - "tabs.results": "", - "tabs.streams": "直播 ({{count}})", - "tabs.subs": "替补", - "tabs.looking": "", + "nav.label": "", + "nav.moreItems": "", + "nav.brackets": "对战表", + "nav.register": "", + "nav.teams": "参赛队伍 ({{count}})", + "nav.streams": "直播 ({{count}})", + "nav.results": "", + "nav.rules": "", + "nav.looking": "", + "nav.subs": "替补", + "nav.divisions": "", + "nav.seeds": "种子", + "nav.admin": "管理", + "findTeam": "", + "registerNow": "", + "fact.format": "", + "fact.bracket": "", + "fact.modes": "", + "fact.tier": "", + "fact.tier.est": "", + "fact.ranked": "", + "fact.ranked.yes": "", + "fact.ranked.yesWithSeason": "", + "fact.ranked.no": "", + "fact.teamSize": "", "pre.steps.header": "参加比赛需完成以下步骤", "pre.steps.name": "队伍名称", "pre.steps.roster": "完整阵容", @@ -35,7 +50,6 @@ "pre.pool.header": "选择地图池", "pre.pool.banned": "禁止", "pre.pool.tiebreaker.short": "决胜局", - "pre.sub.prompt": "没有队伍?您也可以加入替补列表。", "bracket.type.DE_WINNERS": "胜者组 Round", "bracket.type.DE_LOSERS": "败者组 Round", "bracket.type.SE": "单败制 Round",