Skip to content

Commit

Permalink
incremental bracket updates
Browse files Browse the repository at this point in the history
  • Loading branch information
craigkai committed Feb 16, 2024
1 parent c2da8a8 commit f4608f8
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 350 deletions.
4 changes: 4 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ declare global {
matches_ref_fkey: { name: string };
};

interface TeamScores {
[key: string]: number;
}

type UserMatch = Partial<MatchRow> & {
court: number;
round: number;
Expand Down
185 changes: 85 additions & 100 deletions src/components/Bracket.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,110 @@
import { Matches } from '$lib/matches';
import type { Teams } from '$lib/teams';
import type { RealtimeChannel } from '@supabase/supabase-js';
import { Button, Spinner } from 'flowbite-svelte';
export let tournament: Event;
export let matches: Matches;
export let teams: Teams;
export let readOnly: boolean = true;
const teamNames = teams.teams.map((team) => team.name);
matches.loadBracketMatches();
const loadingPromise = $matches.loadBracketMatches();
const numRounds = teamNames.length / 2 + (teamNames.length % 2);
let matchesSubscription: RealtimeChannel | undefined;
async function subscribeToMatches() {
matchesSubscription = await matches.subscribeToBracketMatches();
matchesSubscription = await $matches.subscribeToBracketMatches();
}
subscribeToMatches();
// TODO: Create a listener for when bracket macthes are updated, and auto create the next match in the bracket.
async function handleGenerateBracket() {
await $matches.createBracketMatches(tournament, teams.teams);
}
// TODO: Allow bracket matches to be edited.
</script>

<div class="container">
<div class="tournament-bracket tournament-bracket--rounded">
{#each Array(numRounds) as _, i}
{@const matchesInRound = matches?.bracketMatches?.filter((match) => match.round === i) || []}
<div class="tournament-bracket__round tournament-bracket__round--quarterfinals">
<h3 class="tournament-bracket__round-title">Round {i + 1}</h3>
<ul class="tournament-bracket__list">
{#if matchesInRound.length != 0}
{#each matchesInRound as match}
{@const team1Win =
match.team1_score && match.team2_score
? match.team1_score > match.team2_score
: false}
{@const team2Win = !team1Win && match.team1_score && match.team2_score}
<li class="tournament-bracket__item">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="tournament-bracket__match" tabindex="0">
<table class="tournament-bracket__table">
<thead class="sr-only">
<tr>
<th>Team</th>
<th>Score</th>
</tr>
</thead>
<tbody class="tournament-bracket__content">
<tr
class:tournament-bracket__team--winner={team1Win}
class="tournament-bracket__team"
>
<td class="tournament-bracket__country">
<abbr class="tournament-bracket__code" title="team1"
>{match.matches_team1_fkey.name}</abbr
>
</td>
<td class="tournament-bracket__score">
<span class="tournament-bracket__number">{match?.team1_score}</span>
</td>
</tr>
<tr
class:tournament-bracket__team--winner={team2Win}
class="tournament-bracket__team"
>
<td class="tournament-bracket__country">
<abbr class="tournament-bracket__code" title="team1"
>{match.matches_team2_fkey.name}</abbr
>
</td>
<td class="tournament-bracket__score">
<span class="tournament-bracket__number">{match?.team2_score}</span>
</td>
</tr>
</tbody>
</table>
</div>
</li>
{/each}
{/if}
</ul>
</div>
{/each}
{#await loadingPromise}
<div class="h-screen flex flex-col items-center place-content-center">
<Spinner />
</div>
{:then}
<div class="container">
<div class="tournament-bracket tournament-bracket--rounded">
{#each Array(numRounds) as _, i}
{@const matchesInRound =
$matches?.bracketMatches?.filter((match) => match.round === i) || []}
<div class="tournament-bracket__round tournament-bracket__round--quarterfinals">
<h3 class="tournament-bracket__round-title">Round {i + 1}</h3>
<ul class="tournament-bracket__list">
{#if matchesInRound.length != 0}
{#each matchesInRound as match}
{@const team1Win =
match.team1_score && match.team2_score
? match.team1_score > match.team2_score
: false}
{@const team2Win = !team1Win && match.team1_score && match.team2_score}
<li class="tournament-bracket__item">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="tournament-bracket__match" tabindex="0">
<table class="tournament-bracket__table">
<thead class="sr-only">
<tr>
<th>Team</th>
<th>Score</th>
</tr>
</thead>
<tbody class="tournament-bracket__content">
<tr
class:tournament-bracket__team--winner={team1Win}
class="tournament-bracket__team"
>
<td class="tournament-bracket__country">
<abbr class="tournament-bracket__code" title="team1"
>{match.matches_team1_fkey.name}</abbr
>
</td>
<td class="tournament-bracket__score">
<span class="tournament-bracket__number">{match?.team1_score || 0}</span
>
</td>
</tr>
<tr
class:tournament-bracket__team--winner={team2Win}
class="tournament-bracket__team"
>
<td class="tournament-bracket__country">
<abbr class="tournament-bracket__code" title="team1"
>{match.matches_team2_fkey.name}</abbr
>
</td>
<td class="tournament-bracket__score">
<span class="tournament-bracket__number">{match?.team2_score || 0}</span
>
</td>
</tr>
</tbody>
</table>
</div>
</li>
{/each}
{/if}
</ul>
</div>
{/each}
</div>
</div>

<div class="flex flex-col items-center">
{#if !readOnly}
{#if !matches?.bracketMatches || matches?.bracketMatches?.length === 0}
<Button color="light" on:click={handleGenerateBracket}>Generate initial bracket</Button>
{/if}
{/if}
</div>
</div>
{/await}

<style lang="less">
@breakpoint-xs: 24em;
Expand All @@ -101,48 +122,12 @@
}
}
html {
font-size: 15px;
@media (min-width: @breakpoint-sm) {
font-size: 14px;
}
@media (min-width: @breakpoint-md) {
font-size: 15px;
}
@media (min-width: @breakpoint-lg) {
font-size: 16px;
}
}
body {
background-color: #f1f1f1;
font-family: 'Work Sans', 'Helvetica Neue', Arial, sans-serif;
}
.container {
width: 90%;
min-width: 18em;
margin: 20px auto;
}
h1,
h2 {
text-align: center;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5em;
}
h2 {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 2em;
}
.sr-only {
position: absolute;
width: 1px;
Expand Down
2 changes: 0 additions & 2 deletions src/components/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import type { PageData } from '$types';
import { Hamburger } from 'svelte-hamburgers';
import { DarkMode } from 'flowbite-svelte';
import { error } from '@sveltejs/kit';
import { goto } from '$app/navigation';
export let data: PageData;
let { supabase } = data;
Expand Down
4 changes: 0 additions & 4 deletions src/components/Standings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
export let teams: Teams;
export let defaultTeam: string;
interface TeamScores {
[key: string]: number;
}
let teamScores: TeamScores = {};
const scoring = event.scoring;
Expand Down
79 changes: 75 additions & 4 deletions src/lib/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RoundRobin } from './roundRobin';
import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
import { writable, type Unsubscriber, type Invalidator, type Subscriber } from 'svelte/store';
import { Base } from './base';
import { Event } from './event';

export class Matches extends Base {
private databaseService: MatchesSupabaseDatabaseService;
Expand Down Expand Up @@ -70,6 +71,12 @@ export class Matches extends Base {
const old = payload.old as MatchRow;
const updated = payload.new as MatchRow;

// If we don't have the pool matches loaded, load them
if (Object.keys(old).length === 0) {
self.load();
return;
}

const matchIndex = self.matches?.findIndex((m: MatchRow) => m.id === old.id);
if (matchIndex !== undefined && matchIndex !== -1) {
if (self.matches) {
Expand All @@ -90,7 +97,8 @@ export class Matches extends Base {
}
}

async bracketMatchUpdated(self: Matches,
async bracketMatchUpdated(
self: Matches,
payload: RealtimePostgresChangesPayload<{
[key: string]: MatchRow;
}>
Expand All @@ -101,6 +109,12 @@ export class Matches extends Base {
const old = payload.old as MatchRow;
const updated = payload.new as MatchRow;

// If we don't have the bracket matches loaded, load them
if (Object.keys(old).length === 0) {
self.loadBracketMatches();
return;
}

const matchIndex = self.bracketMatches?.findIndex((m: MatchRow) => m.id === old.id);
if (matchIndex !== undefined && matchIndex !== -1) {
if (self.bracketMatches) {
Expand All @@ -120,9 +134,6 @@ export class Matches extends Base {
self.handleError(400, 'Failed to find bracketMatches to update.');
}

// We want to generate the next round of matches when the previous round is complete
console.error("Bracket match updated")

return;
}

Expand Down Expand Up @@ -325,6 +336,66 @@ export class Matches extends Base {
// Choose a referee from the remaining available teams
return Number(availableTeams[0]);
}

async createBracketMatches(event: Event, teams: TeamRow[]) {
if (!((teams.length & (teams.length - 1)) === 0)) {
this.handleError(
400,
'Number of teams must be a power of 2 for a single-elimination bracket.'
);
}

let teamScores: TeamScores = {};
this?.matches?.forEach((match: MatchRow) => {
// We only care about pool play not bracket/playoff matches
if (match.team1_score && match.team2_score) {
if (!teamScores[match.matches_team1_fkey.name]) {
teamScores[match.matches_team1_fkey.name] = 0;
}

if (!teamScores[match.matches_team2_fkey.name]) {
teamScores[match.matches_team2_fkey.name] = 0;
}

if (event?.scoring === 'points') {
teamScores[match.matches_team1_fkey.name] += match?.team1_score || 0;
teamScores[match.matches_team2_fkey.name] += match?.team2_score || 0;
} else {
teamScores[match.matches_team1_fkey.name] +=
match.team1_score > match.team2_score ? 1 : 0;
teamScores[match.matches_team2_fkey.name] +=
match.team2_score > match.team1_score ? 1 : 0;
}
}
});
const orderedTeamScores = Object.keys(teamScores).sort((a, b) => teamScores[b] - teamScores[a]);
const matchups: any[] = [];

// Generate matchups
let court = 0;
const numCourts = event.courts;
let round = 0;
for (let i = 0; i < orderedTeamScores.length; i += 2) {
if (court === numCourts) {
round = round + 1;
court = 0;
}

const matchup: Partial<MatchRow> = {
team1: teams.find((t) => t.name === orderedTeamScores[i])?.id as number,
team2: teams.find((t) => t.name === orderedTeamScores[orderedTeamScores.length - 1 - i])
?.id as number,
event_id: this.event_id,
type: 'bracket',
round: round
};
court = court + 1;
matchups.push(matchup);
}
this.bracketMatches = await this.databaseService.insertMatches(matchups);

return this.bracketMatches;
}
}

if (import.meta.vitest) {
Expand Down
2 changes: 1 addition & 1 deletion src/routes/protected-routes/events/[slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<Standings event={tournament} {matches} {teams} defaultTeam="" />
</TabItem>
<TabItem title="Bracket">
<Bracket {tournament} {matches} {teams} />
<Bracket {tournament} {matches} {teams} readOnly={false} />
</TabItem>
</Tabs>
{/if}
Expand Down
Loading

0 comments on commit f4608f8

Please sign in to comment.