Various bugfixes (#77)

This commit is contained in:
Erik Vroon
2023-01-16 07:35:44 -08:00
committed by GitHub
parent f85c5e31c8
commit f03bf6cb92
40 changed files with 573 additions and 247 deletions

View File

@@ -9,13 +9,19 @@ updates:
directory: "/backend"
schedule:
interval: "daily"
ignore:
- update-types: ["version-update:semver-patch"]
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "daily"
ignore:
- update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
ignore:
- update-types: ["version-update:semver-patch"]

View File

@@ -18,7 +18,7 @@ sudo pg_createcluster -u postgres -p 5532 13 bracket
pg_ctlcluster 13 bracket start
```
Subsequently, create a new `bracket_dev` database (and `bracket_ci` for tests):
Subsequently, create a new `bracket_dev` database:
```shell
sudo -Hu postgres psql -p 5532
CREATE USER bracket_ci WITH PASSWORD 'bracket_ci';

View File

@@ -4,37 +4,37 @@ verify_ssl = true
name = "pypi"
[packages]
aiopg = "1.4.0"
fastapi = "0.88.0"
fastapi-cache2 = "0.2.0"
gunicorn = "20.1.0"
uvicorn = "0.20.0"
starlette = "0.22.0"
aiopg = ">=1.4.0"
fastapi = ">=0.88.0"
fastapi-cache2 = ">=0.2.0"
gunicorn = ">=20.1.0"
uvicorn = ">=0.20.0"
starlette = ">=0.22.0"
sqlalchemy = "<2.0"
sqlalchemy-stubs = "0.4"
pydantic = "1.10.4"
heliclockter = "1.0.4"
alembic = "1.9.1"
types-simplejson = "3.18.0"
python-dotenv = "0.21.0"
databases = {extras = ["asyncpg"], version = "0.7.0"}
passlib = "1.7.4"
sqlalchemy-stubs = ">=0.4"
pydantic = ">=1.10.4"
heliclockter = ">=1.0.4"
alembic = ">=1.9.1"
types-simplejson = ">=3.18.0"
python-dotenv = ">=0.21.0"
databases = {extras = ["asyncpg"], version = ">=0.7.0"}
passlib = ">=1.7.4"
types-passlib = "*"
pyjwt = "2.6.0"
click = "8.1.3"
python-multipart = "*"
parameterized = "*"
pyjwt = ">=2.6.0"
click = ">=8.1.3"
python-multipart = ">=0.0.5"
parameterized = ">=0.8.1"
[dev-packages]
mypy = "0.991"
black = "22.12.0"
isort = "5.11.4"
pylint = "2.15.10"
pytest = "7.2.0"
pytest-cov = "4.0.0"
pytest-asyncio = "0.20.3"
aiohttp = "3.8.3"
aioresponses = "0.7.4"
mypy = ">=0.991"
black = ">=22.12.0"
isort = ">=5.11.4"
pylint = ">=2.15.10"
pytest = ">=7.2.0"
pytest-cov = ">=4.0.0"
pytest-asyncio = ">=0.20.3"
aiohttp = ">=3.8.3"
aioresponses = ">=0.7.4"
[requires]
python_version = "3.10"

View File

@@ -22,32 +22,33 @@ def calculate_elo_per_player(rounds: list[RoundWithMatches]) -> defaultdict[int,
player_x_elo: defaultdict[int, PlayerStatistics] = defaultdict(PlayerStatistics)
for round in rounds:
for match in round.matches:
for team_index, team in enumerate(match.teams):
for player in team.players:
team_score = match.team1_score if team_index == 0 else match.team2_score
was_draw = match.team1_score == match.team2_score
has_won = not was_draw and team_score == max(
match.team1_score, match.team2_score
)
if not round.is_draft:
for match in round.matches:
for team_index, team in enumerate(match.teams):
for player in team.players:
team_score = match.team1_score if team_index == 0 else match.team2_score
was_draw = match.team1_score == match.team2_score
has_won = not was_draw and team_score == max(
match.team1_score, match.team2_score
)
if has_won:
player_x_elo[assert_some(player.id)].wins += 1
player_x_elo[assert_some(player.id)].swiss_score += Decimal('1.00')
elif was_draw:
player_x_elo[assert_some(player.id)].draws += 1
player_x_elo[assert_some(player.id)].swiss_score += Decimal('0.50')
else:
player_x_elo[assert_some(player.id)].losses += 1
if has_won:
player_x_elo[assert_some(player.id)].wins += 1
player_x_elo[assert_some(player.id)].swiss_score += Decimal('1.00')
elif was_draw:
player_x_elo[assert_some(player.id)].draws += 1
player_x_elo[assert_some(player.id)].swiss_score += Decimal('0.50')
else:
player_x_elo[assert_some(player.id)].losses += 1
player_x_elo[assert_some(player.id)].elo_score += team_score
player_x_elo[assert_some(player.id)].elo_score += team_score
return player_x_elo
async def recalculate_elo_for_tournament_id(tournament_id: int) -> None:
rounds_response = await get_rounds_with_matches(tournament_id)
elo_per_player = calculate_elo_per_player(rounds_response.data)
elo_per_player = calculate_elo_per_player(rounds_response)
for player_id, statistics in elo_per_player.items():
await database.execute(

View File

@@ -22,14 +22,14 @@ async def get_possible_upcoming_matches(
) -> list[SuggestedMatch]:
suggestions: list[SuggestedMatch] = []
rounds_response = await get_rounds_with_matches(tournament_id)
draft_round = next((round for round in rounds_response.data if round.is_draft), None)
draft_round = next((round for round in rounds_response if round.is_draft), None)
if draft_round is None:
raise HTTPException(400, 'There is no draft round, so no matches can be scheduled.')
teams = await get_teams_with_members(tournament_id, only_active_teams=True)
for i, team1 in enumerate(teams.data):
for j, team2 in enumerate(teams.data[i + 1 :]):
for i, team1 in enumerate(teams):
for j, team2 in enumerate(teams[i + 1 :]):
team_already_scheduled = any(
(
True

View File

@@ -16,6 +16,7 @@ class Match(BaseModelORM):
team2_id: int
team1_score: int
team2_score: int
label: str
class UpcomingMatch(BaseModel):
@@ -40,12 +41,14 @@ class MatchBody(BaseModelORM):
round_id: int
team1_score: int = 0
team2_score: int = 0
label: str
class MatchCreateBody(BaseModelORM):
round_id: int
team1_id: int
team2_id: int
label: str
class MatchToInsert(MatchCreateBody):

View File

@@ -5,6 +5,7 @@ from pydantic import validator
from bracket.models.db.match import Match, MatchWithTeamDetails
from bracket.models.db.shared import BaseModelORM
from bracket.utils.types import assert_some
class Round(BaseModelORM):
@@ -29,6 +30,9 @@ class RoundWithMatches(Round):
return values
def get_team_ids(self) -> set[int]:
return {assert_some(team.id) for match in self.matches for team in match.teams}
class RoundBody(BaseModelORM):
name: str

View File

@@ -4,10 +4,11 @@ from heliclockter import datetime_utc
from bracket.database import database
from bracket.logic.elo import recalculate_elo_for_tournament_id
from bracket.logic.upcoming_matches import get_possible_upcoming_matches
from bracket.models.db.match import MatchBody, MatchCreateBody, MatchFilter, MatchToInsert
from bracket.models.db.match import Match, MatchBody, MatchCreateBody, MatchFilter, MatchToInsert
from bracket.models.db.user import UserPublic
from bracket.routes.auth import user_authenticated_for_tournament
from bracket.routes.models import SuccessResponse, UpcomingMatchesResponse
from bracket.routes.util import match_dependency
from bracket.schema import matches
router = APIRouter()
@@ -24,11 +25,13 @@ async def get_matches_to_schedule(
@router.delete("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse)
async def delete_match(
tournament_id: int, match_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
tournament_id: int,
_: UserPublic = Depends(user_authenticated_for_tournament),
match: Match = Depends(match_dependency),
) -> SuccessResponse:
await database.execute(
query=matches.delete().where(
matches.c.id == match_id and matches.c.tournament_id == tournament_id
matches.c.id == match.id and matches.c.tournament_id == tournament_id
),
)
await recalculate_elo_for_tournament_id(tournament_id)
@@ -37,7 +40,6 @@ async def delete_match(
@router.post("/tournaments/{tournament_id}/matches", response_model=SuccessResponse)
async def create_match(
tournament_id: int,
match_body: MatchCreateBody,
_: UserPublic = Depends(user_authenticated_for_tournament),
) -> SuccessResponse:
@@ -54,12 +56,12 @@ async def create_match(
@router.patch("/tournaments/{tournament_id}/matches/{match_id}", response_model=SuccessResponse)
async def update_match_by_id(
tournament_id: int,
match_id: int,
match_body: MatchBody,
_: UserPublic = Depends(user_authenticated_for_tournament),
match: Match = Depends(match_dependency),
) -> SuccessResponse:
await database.execute(
query=matches.update().where(matches.c.id == match_id),
query=matches.update().where(matches.c.id == match.id),
values=match_body.dict(),
)
await recalculate_elo_for_tournament_id(tournament_id)

View File

@@ -1,15 +1,17 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from heliclockter import datetime_utc
from starlette import status
from bracket.database import database
from bracket.logic.elo import recalculate_elo_for_tournament_id
from bracket.models.db.round import RoundBody, RoundToInsert
from bracket.models.db.round import Round, RoundBody, RoundToInsert, RoundWithMatches
from bracket.models.db.user import UserPublic
from bracket.routes.auth import (
user_authenticated_for_tournament,
user_authenticated_or_public_dashboard,
)
from bracket.routes.models import RoundsWithMatchesResponse, SuccessResponse
from bracket.routes.util import round_dependency, round_with_matches_dependency
from bracket.schema import rounds
from bracket.utils.sql import get_next_round_name, get_rounds_with_matches
@@ -26,15 +28,24 @@ async def get_rounds(
tournament_id, no_draft_rounds=user is None or no_draft_rounds
)
if user is not None:
return rounds
return RoundsWithMatchesResponse(data=rounds)
return RoundsWithMatchesResponse(data=[round_ for round_ in rounds.data if not round_.is_draft])
return RoundsWithMatchesResponse(data=[round_ for round_ in rounds if not round_.is_draft])
@router.delete("/tournaments/{tournament_id}/rounds/{round_id}", response_model=SuccessResponse)
async def delete_round(
tournament_id: int, round_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
tournament_id: int,
round_id: int,
_: UserPublic = Depends(user_authenticated_for_tournament),
round_with_matches: RoundWithMatches = Depends(round_with_matches_dependency),
) -> SuccessResponse:
if len(round_with_matches.matches) > 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Round contains matches, delete those first",
)
await database.execute(
query=rounds.delete().where(
rounds.c.id == round_id and rounds.c.tournament_id == tournament_id
@@ -65,6 +76,7 @@ async def update_round_by_id(
round_id: int,
round_body: RoundBody,
_: UserPublic = Depends(user_authenticated_for_tournament),
round: Round = Depends(round_dependency),
) -> SuccessResponse:
values = {'tournament_id': tournament_id, 'round_id': round_id}
query = '''

View File

@@ -1,15 +1,18 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from heliclockter import datetime_utc
from starlette import status
from bracket.database import database
from bracket.logic.elo import recalculate_elo_for_tournament_id
from bracket.models.db.team import Team, TeamBody, TeamToInsert
from bracket.models.db.team import Team, TeamBody, TeamToInsert, TeamWithPlayers
from bracket.models.db.user import UserPublic
from bracket.routes.auth import user_authenticated_for_tournament
from bracket.routes.models import SingleTeamResponse, SuccessResponse, TeamsWithPlayersResponse
from bracket.routes.util import team_dependency, team_with_players_dependency
from bracket.schema import players, teams
from bracket.utils.db import fetch_one_parsed
from bracket.utils.sql import get_teams_with_members
from bracket.utils.sql import get_rounds_with_matches, get_teams_with_members
from bracket.utils.types import assert_some
router = APIRouter()
@@ -38,30 +41,30 @@ async def update_team_members(team_id: int, tournament_id: int, player_ids: list
async def get_teams(
tournament_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
) -> TeamsWithPlayersResponse:
return await get_teams_with_members(tournament_id)
return TeamsWithPlayersResponse.parse_obj({'data': await get_teams_with_members(tournament_id)})
@router.patch("/tournaments/{tournament_id}/teams/{team_id}", response_model=SingleTeamResponse)
async def update_team_by_id(
tournament_id: int,
team_id: int,
team_body: TeamBody,
_: UserPublic = Depends(user_authenticated_for_tournament),
team: Team = Depends(team_dependency),
) -> SingleTeamResponse:
await database.execute(
query=teams.update().where(
(teams.c.id == team_id) & (teams.c.tournament_id == tournament_id)
(teams.c.id == team.id) & (teams.c.tournament_id == tournament_id)
),
values=team_body.dict(exclude={'player_ids'}),
)
await update_team_members(team_id, tournament_id, team_body.player_ids)
await update_team_members(assert_some(team.id), tournament_id, team_body.player_ids)
return SingleTeamResponse(
data=await fetch_one_parsed(
database,
Team,
teams.select().where(
(teams.c.id == team_id) & (teams.c.tournament_id == tournament_id)
(teams.c.id == team.id) & (teams.c.tournament_id == tournament_id)
),
)
)
@@ -69,11 +72,27 @@ async def update_team_by_id(
@router.delete("/tournaments/{tournament_id}/teams/{team_id}", response_model=SuccessResponse)
async def delete_team(
tournament_id: int, team_id: int, _: UserPublic = Depends(user_authenticated_for_tournament)
tournament_id: int,
_: UserPublic = Depends(user_authenticated_for_tournament),
team: TeamWithPlayers = Depends(team_with_players_dependency),
) -> SuccessResponse:
rounds = await get_rounds_with_matches(tournament_id, no_draft_rounds=False)
for round in rounds:
if team.id in round.get_team_ids():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Could not delete team that participates in matches in the tournament",
)
if len(team.players):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Could not delete team that still has players in it",
)
await database.execute(
query=teams.delete().where(
teams.c.id == team_id and teams.c.tournament_id == tournament_id
teams.c.id == team.id and teams.c.tournament_id == tournament_id
),
)
await recalculate_elo_for_tournament_id(tournament_id)
@@ -91,7 +110,7 @@ async def create_team(
values=TeamToInsert(
**team_to_insert.dict(exclude={'player_ids'}),
created=datetime_utc.now(),
tournament_id=tournament_id
tournament_id=tournament_id,
).dict(),
)
await update_team_members(last_record_id, tournament_id, team_to_insert.player_ids)

View File

@@ -0,0 +1,84 @@
from fastapi import HTTPException
from starlette import status
from bracket.database import database
from bracket.models.db.match import Match
from bracket.models.db.round import Round, RoundWithMatches
from bracket.models.db.team import Team, TeamWithPlayers
from bracket.schema import matches, rounds, teams
from bracket.utils.db import fetch_one_parsed
from bracket.utils.sql import get_rounds_with_matches, get_teams_with_members
async def round_dependency(tournament_id: int, round_id: int) -> Round:
round_ = await fetch_one_parsed(
database,
Round,
rounds.select().where(rounds.c.id == round_id and matches.c.tournament_id == tournament_id),
)
if round_ is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find round with id {round_id}",
)
return round_
async def round_with_matches_dependency(tournament_id: int, round_id: int) -> RoundWithMatches:
rounds = await get_rounds_with_matches(tournament_id, no_draft_rounds=False, round_id=round_id)
if len(rounds) < 1:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find round with id {round_id}",
)
return rounds[0]
async def match_dependency(tournament_id: int, match_id: int) -> Match:
match = await fetch_one_parsed(
database,
Match,
matches.select().where(
matches.c.id == match_id and matches.c.tournament_id == tournament_id
),
)
if match is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find match with id {match_id}",
)
return match
async def team_dependency(tournament_id: int, team_id: int) -> Team:
team = await fetch_one_parsed(
database,
Team,
teams.select().where(teams.c.id == team_id and teams.c.tournament_id == tournament_id),
)
if team is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find team with id {team_id}",
)
return team
async def team_with_players_dependency(tournament_id: int, team_id: int) -> TeamWithPlayers:
teams = await get_teams_with_members(tournament_id, team_id=team_id)
if len(teams) < 1:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find team with id {team_id}",
)
return teams[0]

View File

@@ -47,6 +47,7 @@ matches = Table(
Column('team2_id', BigInteger, ForeignKey('teams.id'), nullable=False),
Column('team1_score', Integer, nullable=False),
Column('team2_score', Integer, nullable=False),
Column('label', String, nullable=False),
)
teams = Table(

View File

@@ -56,6 +56,7 @@ DUMMY_MATCH1 = Match(
team2_id=2,
team1_score=11,
team2_score=22,
label='Court 1 | 11:00 - 11:20',
)
DUMMY_MATCH2 = Match(
@@ -65,6 +66,7 @@ DUMMY_MATCH2 = Match(
team2_id=4,
team1_score=9,
team2_score=6,
label='Court 2 | 11:00 - 11:20',
)
DUMMY_MATCH3 = Match(
@@ -74,6 +76,7 @@ DUMMY_MATCH3 = Match(
team2_id=4,
team1_score=23,
team2_score=26,
label='Court 1 | 11:30 - 11:50',
)
DUMMY_MATCH4 = Match(
@@ -83,6 +86,7 @@ DUMMY_MATCH4 = Match(
team2_id=3,
team1_score=43,
team2_score=45,
label='Court 2 | 11:30 - 11:50',
)
DUMMY_USER = User(

View File

@@ -4,12 +4,16 @@ from bracket.database import database
from bracket.models.db.round import RoundWithMatches
from bracket.models.db.team import TeamWithPlayers
from bracket.routes.models import RoundsWithMatchesResponse, TeamsWithPlayersResponse
from bracket.utils.types import dict_without_none
async def get_rounds_with_matches(
tournament_id: int, no_draft_rounds: bool = False
) -> RoundsWithMatchesResponse:
tournament_id: int,
no_draft_rounds: bool = False,
round_id: int | None = None,
) -> list[RoundWithMatches]:
draft_filter = 'AND rounds.is_draft IS FALSE' if no_draft_rounds else ''
round_filter = 'AND rounds.id = :round_id' if round_id is not None else ''
query = f'''
WITH teams_with_players AS (
SELECT DISTINCT ON (teams.id)
@@ -34,12 +38,12 @@ async def get_rounds_with_matches(
LEFT JOIN matches_with_teams m on rounds.id = m.round_id
WHERE rounds.tournament_id = :tournament_id
{draft_filter}
{round_filter}
GROUP BY rounds.id
'''
result = await database.fetch_all(query=query, values={'tournament_id': tournament_id})
return RoundsWithMatchesResponse.parse_obj(
{'data': [RoundWithMatches.parse_obj(x._mapping) for x in result]}
)
values = dict_without_none({'tournament_id': tournament_id, 'round_id': round_id})
result = await database.fetch_all(query=query, values=values)
return [RoundWithMatches.parse_obj(x._mapping) for x in result]
async def get_next_round_name(database: Database, tournament_id: int) -> str:
@@ -54,21 +58,22 @@ async def get_next_round_name(database: Database, tournament_id: int) -> str:
async def get_teams_with_members(
tournament_id: int, *, only_active_teams: bool = False
) -> TeamsWithPlayersResponse:
teams_filter = 'AND teams.active IS TRUE' if only_active_teams else ''
tournament_id: int, *, only_active_teams: bool = False, team_id: int | None = None
) -> list[TeamWithPlayers]:
active_team_filter = 'AND teams.active IS TRUE' if only_active_teams else ''
team_id_filter = 'AND teams.id = :team_id' if team_id is not None else ''
query = f'''
SELECT teams.*, to_json(array_agg(players.*)) AS players
FROM teams
LEFT JOIN players ON players.team_id = teams.id
WHERE teams.tournament_id = :tournament_id
{teams_filter}
{active_team_filter}
{team_id_filter}
GROUP BY teams.id;
'''
result = await database.fetch_all(query=query, values={'tournament_id': tournament_id})
return TeamsWithPlayersResponse.parse_obj(
{'data': [TeamWithPlayers.parse_obj(x._mapping) for x in result]}
)
values = dict_without_none({'tournament_id': tournament_id, 'team_id': team_id})
result = await database.fetch_all(query=query, values=values)
return [TeamWithPlayers.parse_obj(x._mapping) for x in result]
async def get_user_access_to_tournament(tournament_id: int, user_id: int) -> bool:

View File

@@ -31,3 +31,7 @@ class EnumAutoStr(EnumValues):
def assert_some(result: T | None) -> T:
assert result is not None
return result
def dict_without_none(input: dict[Any, Any]) -> dict[Any, Any]:
return {k: v for k, v in input.items() if v is not None}

View File

@@ -32,6 +32,7 @@ async def test_create_match(
'team1_id': team1_inserted.id,
'team2_id': team2_inserted.id,
'round_id': round_inserted.id,
'label': 'Some label',
}
assert (
await send_tournament_request(
@@ -81,7 +82,12 @@ async def test_update_match(
}
)
) as match_inserted:
body = {'team1_score': 42, 'team2_score': 24, 'round_id': round_inserted.id}
body = {
'team1_score': 42,
'team2_score': 24,
'round_id': round_inserted.id,
'label': 'Some label',
}
assert (
await send_tournament_request(
HTTPMethod.PATCH,
@@ -99,6 +105,7 @@ async def test_update_match(
)
assert patched_match.team1_score == body['team1_score']
assert patched_match.team2_score == body['team2_score']
assert patched_match.label == body['label']
await assert_row_count_and_clear(matches, 1)

View File

@@ -11,7 +11,7 @@ def test_elo_calculation() -> None:
round_ = RoundWithMatches(
tournament_id=1,
created=DUMMY_MOCK_TIME,
is_draft=True,
is_draft=False,
is_active=False,
name='Some round',
matches=[
@@ -22,6 +22,7 @@ def test_elo_calculation() -> None:
team1_score=3,
team2_score=4,
round_id=1,
label='Some label',
team1=TeamWithPlayers(
name='Dummy team 1',
tournament_id=1,

View File

@@ -2,8 +2,6 @@ coverage:
status:
project:
default:
target: 100%
threshold: 100%
informational: true
wait_for_ci: false
require_ci_to_pass: false

View File

@@ -1 +1,110 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="73.701149mm"
height="73.701828mm"
viewBox="0 0 73.701149 73.701828"
version="1.1"
id="svg5"
inkscape:export-filename="/home/erik/code/bracket/frontend/public/favicon.png"
inkscape:export-xdpi="400.06485"
inkscape:export-ydpi="400.06485"
sodipodi:docname="favicon.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="3.2268341"
inkscape:cx="118.53724"
inkscape:cy="143.63924"
inkscape:window-width="2560"
inkscape:window-height="1376"
inkscape:window-x="3840"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showguides="true"
inkscape:guide-bbox="true" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient37147">
<stop
style="stop-color:#6948e8;stop-opacity:1"
offset="0"
id="stop37143" />
<stop
style="stop-color:#9379f5;stop-opacity:1"
offset="1"
id="stop37145" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient37147"
id="linearGradient37149"
x1="197.95337"
y1="200.584"
x2="42.395298"
y2="31.763771"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.31598688,0,0,0.31598688,66.116499,102.77293)" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-66.116493,-102.77294)">
<path
id="path33666"
style="fill:url(#linearGradient37149);fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
d="m 102.9672,102.77294 a 36.850776,36.850776 0 0 0 -36.850707,36.85078 36.850776,36.850776 0 0 0 31.797454,36.48845 l -13.091302,-13.09121 0.639774,-19.35673 -1.781779,7.8368 -9.307407,-9.30749 3.464113,-2.45816 7.625073,3.92885 -2.9291,-5.22051 6.111755,-18.921 9.453135,-5.4637 25.546141,25.54614 -2.52109,-23.28549 18.34328,18.34328 a 36.850776,36.850776 0 0 0 -36.49899,-31.89001 z" />
<path
id="path33606"
style="fill:#5337b9;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
d="m 139.4662,134.66295 -18.34329,-18.34328 2.52109,23.28549 -25.54614,-25.54614 -9.453134,5.4637 -6.111756,18.921 2.9291,5.22051 -0.639774,19.35673 13.091302,13.09121 a 36.850776,36.850776 0 0 0 5.053252,0.3626 36.850776,36.850776 0 0 0 36.85079,-36.8507 36.850776,36.850776 0 0 0 -0.35214,-4.96077 z" />
<path
id="path53"
style="fill:#4629ae;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
d="m 77.836997,139.73538 -3.464112,2.45816 9.307407,9.30749 1.781778,-7.8368 z" />
<path
id="path33645"
style="fill:#7755f5;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
d="m 112.88997,117.52931 -5.05334,1.66942 8.04345,8.04407 1.59908,-5.46492 z" />
<path
id="path33668"
style="fill:#4629ae;fill-opacity:1;fill-rule:evenodd;stroke-width:0.315986"
d="m 112.88997,117.52931 4.58919,4.24857 -1.59908,5.46492 -8.04345,-8.04407 z" />
<g
aria-label="{}"
id="text1281"
style="font-size:46.3909px;line-height:1.25;font-family:monospace;-inkscape-font-specification:monospace;fill:#ffffff;stroke-width:1.15977"
transform="matrix(1.1942812,0,0,1.1942812,-20.004416,-27.126378)">
<path
d="m 98.88979,156.70333 v 4.3265 h -3.397771 q -5.6403,0 -7.565704,-1.67623 -1.902751,-1.67624 -1.902751,-6.68229 v -4.87014 q 0,-3.42042 -1.223198,-4.71157 -1.200546,-1.31381 -4.371799,-1.31381 h -1.404412 v -4.32649 h 1.404412 q 3.171253,0 4.371799,-1.29115 1.223198,-1.29116 1.223198,-4.73423 v -4.87014 q 0,-5.00605 1.902751,-6.65963 1.925404,-1.67623 7.565704,-1.67623 h 3.397771 v 4.30384 h -2.786173 q -2.355787,0 -3.148601,0.95137 -0.792813,0.95138 -0.792813,4.19059 v 4.73423 q 0,3.73754 -1.109938,5.25522 -1.109939,1.51767 -3.964067,1.94805 2.854128,0.47569 3.964067,2.01601 1.109938,1.54032 1.109938,5.25522 v 4.64362 q 0,3.26186 0.792813,4.21324 0.792814,0.97402 3.148601,0.97402 z"
style="font-weight:bold;-inkscape-font-specification:'monospace, Bold'"
id="path33435" />
<path
d="m 107.04444,156.70333 h 2.74087 q 2.35579,0 3.17125,-0.97402 0.81547,-0.97403 0.81547,-4.21324 v -4.64362 q 0,-3.7149 1.10994,-5.25522 1.10993,-1.54032 3.94141,-2.01601 -2.85413,-0.43038 -3.96407,-1.94805 -1.08728,-1.51768 -1.08728,-5.25522 v -4.73423 q 0,-3.19391 -0.81547,-4.16793 -0.79281,-0.97403 -3.17125,-0.97403 h -2.74087 v -4.30384 h 3.39777 q 5.61765,0 7.49775,1.67623 1.90275,1.65358 1.90275,6.65963 v 4.87014 q 0,3.42042 1.2232,4.73423 1.24585,1.29115 4.43975,1.29115 h 1.40441 v 4.32649 h -1.40441 q -3.1939,0 -4.43975,1.31381 -1.2232,1.3138 -1.2232,4.71157 v 4.87014 q 0,5.00605 -1.90275,6.68229 -1.8801,1.67623 -7.49775,1.67623 h -3.39777 z"
style="font-weight:bold;-inkscape-font-specification:'monospace, Bold'"
id="path33437" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -21,7 +21,7 @@ export default function Brackets({
}
const rounds = swrRoundsResponse.data.data.map((round: RoundInterface) => (
<Grid.Col sm={6} lg={3}>
<Grid.Col sm={6} lg={4} xl={3}>
<Round
key={round.id}
tournamentData={tournamentData}

View File

@@ -1,15 +1,26 @@
import { Grid, Tooltip, UnstyledButton, createStyles, useMantineTheme } from '@mantine/core';
import { useState } from 'react';
import {
Badge,
Center,
Grid,
Tooltip,
UnstyledButton,
createStyles,
useMantineTheme,
} from '@mantine/core';
import { Property } from 'csstype';
import React, { useState } from 'react';
import { SWRResponse } from 'swr';
import { MatchInterface } from '../../interfaces/match';
import { TournamentMinimal } from '../../interfaces/tournament';
import MatchModal from '../modals/match_modal';
import Visibility = Property.Visibility;
const useStyles = createStyles((theme) => ({
root: {
width: '100%',
marginTop: '20px',
marginTop: '30px',
},
divider: {
backgroundColor: 'darkgray',
@@ -29,7 +40,18 @@ const useStyles = createStyles((theme) => ({
},
}));
export default function Game({
function MatchBadge({ match }: { match: MatchInterface }) {
const visibility: Visibility = match.label === '' ? 'hidden' : 'visible';
return (
<Center style={{ transform: 'translateY(50%)', visibility }}>
<Badge size="lg" variant="filled">
{match.label}
</Badge>
</Center>
);
}
export default function Match({
swrRoundsResponse,
swrUpcomingMatchesResponse,
tournamentData,
@@ -53,12 +75,16 @@ export default function Game({
const team1_players = match.team1.players.map((player) => player.name).join(', ');
const team2_players = match.team2.players.map((player) => player.name).join(', ');
const team1_players_label = team1_players === '' ? 'No players' : team1_players;
const team2_players_label = team2_players === '' ? 'No players' : team2_players;
const [opened, setOpened] = useState(false);
const bracket = (
<>
<MatchBadge match={match} />
<div className={classes.top} style={team1_style}>
<Tooltip label={team1_players} withArrow color="blue">
<Tooltip label={team1_players_label} withArrow color="blue">
<Grid grow>
<Grid.Col span={10}>{match.team1.name}</Grid.Col>
<Grid.Col span={2}>{match.team1_score}</Grid.Col>
@@ -67,7 +93,7 @@ export default function Game({
</div>
<div className={classes.divider} />
<div className={classes.bottom} style={team2_style}>
<Tooltip label={team2_players} position="bottom" withArrow color="blue">
<Tooltip label={team2_players_label} position="bottom" withArrow color="blue">
<Grid grow>
<Grid.Col span={10}>{match.team2.name}</Grid.Col>
<Grid.Col span={2}>{match.team2_score}</Grid.Col>

View File

@@ -5,7 +5,7 @@ import { SWRResponse } from 'swr';
import { RoundInterface } from '../../interfaces/round';
import { TournamentMinimal } from '../../interfaces/tournament';
import RoundModal from '../modals/round_modal';
import Game from './game';
import Match from './match';
export default function Round({
tournamentData,
@@ -20,16 +20,18 @@ export default function Round({
swrUpcomingMatchesResponse: SWRResponse | null;
readOnly: boolean;
}) {
const games = round.matches.map((match) => (
<Game
key={match.id}
tournamentData={tournamentData}
swrRoundsResponse={swrRoundsResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
match={match}
readOnly={readOnly}
/>
));
const matches = round.matches
.sort((m1, m2) => (m1.label > m2.label ? 1 : 0))
.map((match) => (
<Match
key={match.id}
tournamentData={tournamentData}
swrRoundsResponse={swrRoundsResponse}
swrUpcomingMatchesResponse={swrUpcomingMatchesResponse}
match={match}
readOnly={readOnly}
/>
));
const active_round_style = round.is_active
? {
borderStyle: 'solid',
@@ -68,7 +70,7 @@ export default function Round({
}}
>
<Center>{modal}</Center>
{games}
{matches}
</div>
</div>
);

View File

@@ -1,30 +0,0 @@
import { Group, Progress, Text, useMantineTheme } from '@mantine/core';
interface ELOProps {
elo_score: number;
max_elo_score: number;
}
export function PlayerELOScore({ elo_score, max_elo_score }: ELOProps) {
const theme = useMantineTheme();
const percentageScale = 100.0 / max_elo_score;
return (
<>
<Group position="apart">
<Text size="xs" color="blue" weight={700}>
{elo_score.toFixed(0)}
</Text>
</Group>
<Progress
sections={[
{
value: percentageScale * elo_score,
color: theme.colorScheme === 'dark' ? theme.colors.blue[9] : theme.colors.blue[6],
tooltip: `ELO Score (${(percentageScale * elo_score).toFixed(0)}%)`,
},
]}
/>
</>
);
}

View File

@@ -0,0 +1,34 @@
import { Group, Progress, Text, useMantineTheme } from '@mantine/core';
import { DefaultMantineColor } from '@mantine/styles/lib/theme/types/MantineColor';
interface ScoreProps {
score: number;
max_score: number;
color: DefaultMantineColor;
decimals: number;
}
export function PlayerScore({ score, max_score, color, decimals }: ScoreProps) {
const theme = useMantineTheme();
const percentageScale = 100.0 / max_score;
const base_color = theme.colors[color];
return (
<>
<Group position="apart">
<Text size="xs" color={color} weight={700}>
{score.toFixed(0)}
</Text>
</Group>
<Progress
sections={[
{
value: percentageScale * score,
color: theme.colorScheme === 'dark' ? base_color[9] : base_color[6],
tooltip: `ELO Score (${(percentageScale * score).toFixed(decimals)}%)`,
},
]}
/>
</>
);
}

View File

@@ -1,4 +1,4 @@
import { Button, Modal, NumberInput } from '@mantine/core';
import { Button, Modal, NumberInput, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import React from 'react';
import { SWRResponse } from 'swr';
@@ -27,6 +27,7 @@ export default function MatchModal({
initialValues: {
team1_score: match != null ? match.team1_score : 0,
team2_score: match != null ? match.team2_score : 0,
label: match != null ? match.label : '',
},
validate: {
@@ -45,6 +46,7 @@ export default function MatchModal({
round_id: match.round_id,
team1_score: values.team1_score,
team2_score: values.team2_score,
label: values.label,
};
await updateMatch(tournamentData.id, match.id, newMatch);
await swrRoundsResponse.mutate(null);
@@ -65,6 +67,13 @@ export default function MatchModal({
placeholder={`Score of ${match.team2.name}`}
{...form.getInputProps('team2_score')}
/>
<TextInput
withAsterisk
style={{ marginTop: 20 }}
label="Label for this match"
placeholder="Court 1 | 11:30 - 12:00"
{...form.getInputProps('label')}
/>
<Button fullWidth style={{ marginTop: 20 }} color="green" type="submit">
Save
</Button>

View File

@@ -2,11 +2,12 @@ import {
ActionIcon,
Box,
Group,
Image,
Title,
UnstyledButton,
useMantineColorScheme,
} from '@mantine/core';
import { IconBrackets, IconMoonStars, IconSun } from '@tabler/icons';
import { IconMoonStars, IconSun } from '@tabler/icons';
import { useRouter } from 'next/router';
import React from 'react';
@@ -26,15 +27,17 @@ export function Brand() {
})}
>
<Group position="apart">
<IconBrackets size={36} style={{ marginBottom: 10 }} />
<UnstyledButton>
<Title
onClick={() => {
router.push('/');
}}
>
Bracket
</Title>
<Group>
<Image src="/favicon.svg" width="50px" height="50px" mt="-8px" />
<Title
onClick={() => {
router.push('/');
}}
>
Bracket
</Title>
</Group>
</UnstyledButton>
<ActionIcon variant="default" onClick={() => toggleColorScheme()} size={30}>
{colorScheme === 'dark' ? <IconSun size={16} /> : <IconMoonStars size={16} />}

View File

@@ -5,7 +5,7 @@ import { Player } from '../../interfaces/player';
import { TournamentMinimal } from '../../interfaces/tournament';
import { deletePlayer } from '../../services/player';
import DeleteButton from '../buttons/delete';
import { PlayerELOScore } from '../info/player_elo_score';
import { PlayerScore } from '../info/player_score';
import { PlayerStatistics } from '../info/player_statistics';
import PlayerModal from '../modals/player_modal';
import DateTime from '../utils/datetime';
@@ -23,6 +23,7 @@ export default function PlayersTable({
const tableState = getTableState('name');
const maxELOScore = Math.max(...players.map((player) => player.elo_score));
const maxSwissScore = Math.max(...players.map((player) => player.swiss_score));
if (swrPlayersResponse.error) return <RequestErrorAlert error={swrPlayersResponse.error} />;
@@ -38,7 +39,20 @@ export default function PlayersTable({
<PlayerStatistics wins={player.wins} draws={player.draws} losses={player.losses} />
</td>
<td>
<PlayerELOScore elo_score={player.elo_score} max_elo_score={maxELOScore} />
<PlayerScore
score={player.elo_score}
max_score={maxELOScore}
color="indigo"
decimals={0}
/>
</td>
<td>
<PlayerScore
score={player.swiss_score}
max_score={maxSwissScore}
color="grape"
decimals={1}
/>
</td>
<td>
<PlayerModal
@@ -71,6 +85,9 @@ export default function PlayersTable({
<ThSortable state={tableState} field="elo_score">
ELO score
</ThSortable>
<ThSortable state={tableState} field="swiss_score">
Swiss score
</ThSortable>
<ThNotSortable>{null}</ThNotSortable>
</tr>
</thead>

View File

@@ -57,9 +57,9 @@ export const setSorting = (state: TableState, newSortField: string) => {
export const getTableState = (
initial_sort_field: string,
initial_sort_direection: boolean = true
initial_sort_direction: boolean = true
) => {
const [reversed, setReversed] = useState(initial_sort_direection);
const [reversed, setReversed] = useState(initial_sort_direction);
const [sortField, setSortField] = useState(initial_sort_field);
return {
sortField,

View File

@@ -34,6 +34,7 @@ export default function UpcomingMatchesTable({
team1_id: upcoming_match.team1.id,
team2_id: upcoming_match.team2.id,
round_id,
label: '',
};
await createMatch(tournamentData.id, match_to_schedule);
await swrRoundsResponse.mutate(null);
@@ -45,7 +46,7 @@ export default function UpcomingMatchesTable({
sortTableEntries(m1, m2, tableState)
)
.map((upcoming_match: UpcomingMatchInterface) => (
<tr key={upcoming_match.elo_diff}>
<tr key={`${upcoming_match.team1.id} - ${upcoming_match.team2.id}`}>
<td>
<PlayerList team={upcoming_match.team1} />
</td>
@@ -74,10 +75,10 @@ export default function UpcomingMatchesTable({
<TableLayout>
<thead>
<tr>
<ThSortable state={tableState} field="name">
<ThSortable state={tableState} field="team1.name">
Team 1
</ThSortable>
<ThSortable state={tableState} field="name">
<ThSortable state={tableState} field="team2.name">
Team 2
</ThSortable>
<ThSortable state={tableState} field="elo_diff">

View File

@@ -8,6 +8,7 @@ export interface MatchInterface {
team2_score: number;
team1: TeamInterface;
team2: TeamInterface;
label: string;
}
export interface MatchBodyInterface {
@@ -15,6 +16,7 @@ export interface MatchBodyInterface {
round_id: number;
team1_score: number;
team2_score: number;
label: string;
}
export interface UpcomingMatchInterface {
@@ -28,4 +30,5 @@ export interface MatchCreateBodyInterface {
round_id: number;
team1_id: number;
team2_id: number;
label: string;
}

View File

@@ -5,6 +5,7 @@ export interface Player {
tournament_id: number;
team_id: number;
elo_score: number;
swiss_score: number;
wins: number;
draws: number;
losses: number;

View File

@@ -12,14 +12,16 @@ export default function Login() {
const router = useRouter();
async function attemptLogin(email: string, password: string) {
await performLogin(email, password);
showNotification({
color: 'green',
title: 'Login successful',
message: '',
});
const success = await performLogin(email, password);
if (success) {
showNotification({
color: 'green',
title: 'Login successful',
message: '',
});
await router.push('/');
await router.push('/');
}
}
const form = useForm({
initialValues: {

View File

@@ -8,6 +8,12 @@ import { getTournamentIdFromRouter } from '../../../components/utils/util';
import { Tournament } from '../../../interfaces/tournament';
import { getBaseApiUrl, getRounds, getTournaments } from '../../../services/adapter';
function TournamentLogo({ tournamentDataFull }: { tournamentDataFull: Tournament }) {
return tournamentDataFull.logo_path ? (
<Image radius="lg" src={`${getBaseApiUrl()}/static/${tournamentDataFull.logo_path}`} />
) : null;
}
export default function Dashboard() {
const { tournamentData } = getTournamentIdFromRouter();
const swrRoundsResponse: SWRResponse = getRounds(tournamentData.id, true);
@@ -24,15 +30,12 @@ export default function Dashboard() {
}
return (
<Grid
grow
style={{ marginBottom: '20px', marginTop: '20px', marginLeft: '20px', marginRight: '20px' }}
>
<Grid.Col span={3}>
<Grid grow style={{ margin: '20px' }}>
<Grid.Col span={2}>
<Title>{tournamentDataFull.name}</Title>
<Image radius="lg" src={`${getBaseApiUrl()}/static/${tournamentDataFull.logo_path}`} />
<TournamentLogo tournamentDataFull={tournamentDataFull} />
</Grid.Col>
<Grid.Col span={9}>
<Grid.Col span={10}>
<Brackets
tournamentData={tournamentData}
swrRoundsResponse={swrRoundsResponse}

View File

@@ -4,12 +4,14 @@ import useSWR, { SWRResponse } from 'swr';
const axios = require('axios').default;
export function handleRequestError(error: any) {
showNotification({
color: 'red',
title: 'Default notification',
message: error.response.data.detail.toString(),
});
export function handleRequestError(response: any) {
if (response.response != null && response.response.data.detail != null) {
showNotification({
color: 'red',
title: 'An error occurred',
message: response.response.data.detail.toString(),
});
}
}
export function checkForAuthError(response: any) {
@@ -50,29 +52,23 @@ export function getClubs(): SWRResponse {
return useSWR('clubs', fetcher);
}
export function getTournaments(): SWRResponse<any, any> {
export function getTournaments(): SWRResponse {
return useSWR('tournaments', fetcher);
}
export function getPlayers(
tournament_id: number,
not_in_team: boolean = false
): SWRResponse<any, any> {
export function getPlayers(tournament_id: number, not_in_team: boolean = false): SWRResponse {
return useSWR(`tournaments/${tournament_id}/players?not_in_team=${not_in_team}`, fetcher);
}
export function getTeams(tournament_id: number): SWRResponse<any, any> {
export function getTeams(tournament_id: number): SWRResponse {
return useSWR(`tournaments/${tournament_id}/teams`, fetcher);
}
export function getRounds(
tournament_id: number,
no_draft_rounds: boolean = false
): SWRResponse<any, any> {
export function getRounds(tournament_id: number, no_draft_rounds: boolean = false): SWRResponse {
return useSWR(`tournaments/${tournament_id}/rounds?no_draft_rounds=${no_draft_rounds}`, fetcher);
}
export function getUpcomingMatches(tournament_id: number): SWRResponse<any, any> {
export function getUpcomingMatches(tournament_id: number): SWRResponse {
return useSWR(`tournaments/${tournament_id}/upcoming_matches`, fetcher);
}

View File

@@ -1,32 +0,0 @@
import { createAxios } from './adapter';
export async function createTeam(
tournament_id: number,
name: string,
active: boolean,
player_ids: number[]
) {
await createAxios().post(`tournaments/${tournament_id}/teams`, {
name,
active,
player_ids,
});
}
export async function deleteTeam(tournament_id: number, team_id: number) {
await createAxios().delete(`tournaments/${tournament_id}/teams/${team_id}`);
}
export async function updateTeam(
tournament_id: number,
team_id: number,
name: string,
active: boolean,
player_ids: number[]
) {
await createAxios().patch(`tournaments/${tournament_id}/teams/${team_id}`, {
name,
active,
player_ids,
});
}

View File

@@ -1,12 +1,16 @@
import { MatchBodyInterface, MatchCreateBodyInterface } from '../interfaces/match';
import { createAxios } from './adapter';
import { createAxios, handleRequestError } from './adapter';
export async function createMatch(tournament_id: number, match: MatchCreateBodyInterface) {
return createAxios().post(`tournaments/${tournament_id}/matches`, match);
return createAxios()
.post(`tournaments/${tournament_id}/matches`, match)
.catch((response: any) => handleRequestError(response));
}
export async function deleteMatch(tournament_id: number, match_id: number) {
return createAxios().delete(`tournaments/${tournament_id}/matches/${match_id}`);
return createAxios()
.delete(`tournaments/${tournament_id}/matches/${match_id}`)
.catch((response: any) => handleRequestError(response));
}
export async function updateMatch(
@@ -14,5 +18,7 @@ export async function updateMatch(
match_id: number,
match: MatchBodyInterface
) {
return createAxios().patch(`tournaments/${tournament_id}/matches/${match_id}`, match);
return createAxios()
.patch(`tournaments/${tournament_id}/matches/${match_id}`, match)
.catch((response: any) => handleRequestError(response));
}

View File

@@ -1,14 +1,18 @@
import { createAxios } from './adapter';
import { createAxios, handleRequestError } from './adapter';
export async function createPlayer(tournament_id: number, name: string, team_id: string | null) {
return createAxios().post(`tournaments/${tournament_id}/players`, {
name,
team_id,
});
return createAxios()
.post(`tournaments/${tournament_id}/players`, {
name,
team_id,
})
.catch((response: any) => handleRequestError(response));
}
export async function deletePlayer(tournament_id: number, player_id: number) {
return createAxios().delete(`tournaments/${tournament_id}/players/${player_id}`);
return createAxios()
.delete(`tournaments/${tournament_id}/players/${player_id}`)
.catch((response: any) => handleRequestError(response));
}
export async function updatePlayer(
@@ -17,8 +21,10 @@ export async function updatePlayer(
name: string,
team_id: string | null
) {
return createAxios().patch(`tournaments/${tournament_id}/players/${player_id}`, {
name,
team_id,
});
return createAxios()
.patch(`tournaments/${tournament_id}/players/${player_id}`, {
name,
team_id,
})
.catch((response: any) => handleRequestError(response));
}

View File

@@ -1,14 +1,20 @@
import { RoundInterface } from '../interfaces/round';
import { createAxios } from './adapter';
import { createAxios, handleRequestError } from './adapter';
export async function createRound(tournament_id: number) {
return createAxios().post(`tournaments/${tournament_id}/rounds`);
return createAxios()
.post(`tournaments/${tournament_id}/rounds`)
.catch((response: any) => handleRequestError(response));
}
export async function deleteRound(tournament_id: number, round_id: number) {
return createAxios().delete(`tournaments/${tournament_id}/rounds/${round_id}`);
return createAxios()
.delete(`tournaments/${tournament_id}/rounds/${round_id}`)
.catch((response: any) => handleRequestError(response));
}
export async function updateRound(tournament_id: number, round_id: number, round: RoundInterface) {
return createAxios().patch(`tournaments/${tournament_id}/rounds/${round_id}`, round);
return createAxios()
.patch(`tournaments/${tournament_id}/rounds/${round_id}`, round)
.catch((response: any) => handleRequestError(response));
}

View File

@@ -1,4 +1,4 @@
import { createAxios } from './adapter';
import { createAxios, handleRequestError } from './adapter';
export async function createTeam(
tournament_id: number,
@@ -14,7 +14,9 @@ export async function createTeam(
}
export async function deleteTeam(tournament_id: number, team_id: number) {
await createAxios().delete(`tournaments/${tournament_id}/teams/${team_id}`);
await createAxios()
.delete(`tournaments/${tournament_id}/teams/${team_id}`)
.catch((response: any) => handleRequestError(response));
}
export async function updateTeam(
@@ -24,9 +26,11 @@ export async function updateTeam(
active: boolean,
player_ids: number[]
) {
await createAxios().patch(`tournaments/${tournament_id}/teams/${team_id}`, {
name,
active,
player_ids,
});
await createAxios()
.patch(`tournaments/${tournament_id}/teams/${team_id}`, {
name,
active,
player_ids,
})
.catch((response: any) => handleRequestError(response));
}

View File

@@ -1,4 +1,4 @@
import { createAxios } from './adapter';
import { createAxios, handleRequestError } from './adapter';
export async function performLogin(username: string, password: string) {
const bodyFormData = new FormData();
@@ -6,12 +6,21 @@ export async function performLogin(username: string, password: string) {
bodyFormData.append('username', username);
bodyFormData.append('password', password);
const response = await createAxios().post('token', bodyFormData);
const response = await createAxios()
.post('token', bodyFormData)
.catch((err_response: any) => handleRequestError(err_response));
if (response == null) {
return false;
}
localStorage.setItem('login', JSON.stringify(response.data));
handleRequestError(response);
// Reload axios object.
createAxios();
return response;
return true;
}
export function performLogout() {