diff --git a/README.md b/README.md
index 2791aeb3b..66fb19b90 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ You can give a variation as a flag to the seeding script changing what exactly i
- [ ] Admin can change team captain
- [x] Admin can randomize and rerandomize maps
- [x] Admin can change seeding
-- [ ] Admin can start the tournament
+- [x] Admin can start the tournament
- [ ] Table design to support arbitrary brackets
- [x] Generate SE bracket from seeds
- [x] Generate DE bracket from seeds
diff --git a/app/components/tournament/EliminationBracket.tsx b/app/components/tournament/EliminationBracket.tsx
new file mode 100644
index 000000000..ee76a18c4
--- /dev/null
+++ b/app/components/tournament/EliminationBracket.tsx
@@ -0,0 +1,145 @@
+import classNames from "classnames";
+
+const rounds = [
+ {
+ title: "Round 1",
+ bestOf: 3,
+ status: "DONE",
+ },
+ {
+ title: "Round 2",
+ bestOf: 5,
+ status: "INPROGRESS",
+ },
+ {
+ title: "Semifinals",
+ bestOf: 5,
+ status: "UPCOMING",
+ },
+ {
+ title: "Finals",
+ bestOf: 7,
+ status: "UPCOMING",
+ },
+] as const;
+
+export function EliminationBracket() {
+ return (
+
+
+ {rounds.map((round, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+function RoundInfo(props: {
+ round: {
+ title: string;
+ bestOf: number;
+ status: "DONE" | "INPROGRESS" | "UPCOMING";
+ };
+ isLast?: boolean;
+}) {
+ return (
+
+
+ {props.round.title}
+
+ {props.round.status !== "DONE" && (
+
+ Bo{props.round.bestOf}
+
+ )}
+
+ );
+}
+
+export function Match() {
+ return (
+
+
1
+
+ Team Olive 1
+
+
+ Chimera 1
+
+
+ );
+}
diff --git a/app/hooks/useTournamentRounds/index.ts b/app/hooks/useTournamentRounds/index.ts
index 2947245fd..ad0e0ca67 100644
--- a/app/hooks/useTournamentRounds/index.ts
+++ b/app/hooks/useTournamentRounds/index.ts
@@ -8,6 +8,7 @@ import type {
UseTournamentRoundsState,
} from "./types";
+// TODO: could save this to local storage but need to handle amount of rounds changing
export function useTournamentRounds(args: UseTournamentRoundsArgs) {
return React.useReducer(
(oldState: UseTournamentRoundsState, action: UseTournamentRoundsAction) => {
diff --git a/app/routes/to/$organization.$tournament/bracket.$id.tsx b/app/routes/to/$organization.$tournament/bracket.$id.tsx
new file mode 100644
index 000000000..5bd4580f3
--- /dev/null
+++ b/app/routes/to/$organization.$tournament/bracket.$id.tsx
@@ -0,0 +1,15 @@
+import type { LinksFunction } from "remix";
+import { EliminationBracket } from "~/components/tournament/EliminationBracket";
+import styles from "~/styles/tournament-bracket.css";
+
+export const links: LinksFunction = () => {
+ return [{ rel: "stylesheet", href: styles }];
+};
+
+export default function BracketTabWrapper() {
+ return (
+
+
+
+ );
+}
diff --git a/app/styles/tournament-bracket.css b/app/styles/tournament-bracket.css
new file mode 100644
index 000000000..adf64726d
--- /dev/null
+++ b/app/styles/tournament-bracket.css
@@ -0,0 +1,120 @@
+.tournament-bracket__container {
+ width: 100%;
+}
+
+.tournament-bracket__elim__container {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ margin: 0 auto;
+}
+
+.tournament-bracket__elim__bracket {
+ display: grid;
+ column-gap: 2px;
+ grid-template-columns: repeat(var(--brackets-columns), 250px);
+ overflow-x: auto;
+
+ --brackets-match-height: 100px;
+}
+
+.tournament-bracket__elim__roundInfo {
+ padding: 1.5rem 1rem;
+ background-color: var(--bg-lighter);
+
+ /* TODO: transition: background-color 0.5s; */
+}
+
+.tournament-bracket__elim__roundInfo:first-of-type {
+ border-radius: var(--rounded) 0 0 var(--rounded);
+}
+
+.tournament-bracket__elim__roundInfo.highlighted {
+ background-color: var(--theme);
+ color: var(--button-text);
+}
+
+.tournament-bracket__elim__roundInfo.last {
+ border-radius: 0 var(--rounded) var(--rounded) 0;
+}
+
+.tournament-bracket__elim__roundTitle {
+ font-size: var(--fonts-sm);
+ font-weight: 500;
+}
+
+.tournament-bracket__elim__bestOf {
+ font-size: var(--fonts-xs);
+}
+
+.tournament-bracket__elim__match {
+ display: grid;
+ margin: 1rem 0;
+ column-gap: 5px;
+ grid-template-areas:
+ "round-number team-one"
+ "round-number team-two";
+ grid-template-columns: 10px 1fr;
+ place-items: center;
+ row-gap: 2px;
+}
+
+.tournament-bracket__elim__roundNumber {
+ font-size: var(--fonts-xs);
+ grid-area: round-number;
+}
+
+.tournament-bracket__elim__team {
+ display: flex;
+ width: 200px;
+ justify-content: space-between;
+ padding: 0.25rem 0.75rem;
+ background-color: var(--bg-lighter);
+ font-size: var(--fonts-sm);
+ place-self: flex-start;
+}
+
+.tournament-bracket__elim__teamOne {
+ border-radius: var(--rounded) var(--rounded) 0 0;
+ grid-area: team-one;
+}
+
+.tournament-bracket__elim__teamTwo {
+ border-radius: 0 0 var(--rounded) var(--rounded);
+ grid-area: team-two;
+}
+
+.tournament-bracket__elim__score {
+ font-weight: bold;
+}
+
+.tournament-bracket__elim__column {
+ display: flex;
+}
+
+.tournament-bracket__elim__matches {
+ display: flex;
+ height: calc(var(--brackets-match-height) * var(--brackets-matches));
+ flex-direction: column;
+ justify-content: space-around;
+}
+
+.tournament-bracket__elim__lines {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ padding-left: 7px;
+}
+
+.tournament-bracket__elim__lines > div {
+ width: calc(var(--brackets-match-height) / 4);
+ height: calc(
+ 2px + var(--brackets-match-height) *
+ (var(--brackets-matches) / var(--brackets-column-matches))
+ );
+ border-top: 2px solid var(--theme-transparent);
+ border-bottom: var(--brackets-bottom-border-length, 2px) solid
+ var(--theme-transparent);
+ border-inline-end: 2px solid var(--theme-transparent);
+ border-radius: 0 var(--rounded) var(--rounded) 0;
+}