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 ? (
+ <>
+
+
+ {rightFacts.map((fact) => (
+
+ ))}
+
+ >
+ ) : null}
+
+ );
+}
+
+function Card({ label, value }: FactCardItem) {
+ return (
+
+ );
+}
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 (
+
+ );
+}
+
+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.icon}
+
+ {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",