From 2b031b4b2471cdc3ac5d7456d89f2423a8f86a32 Mon Sep 17 00:00:00 2001
From: ForkBench <robinvandemerghel@protonmail.com>
Date: Wed, 10 Jul 2024 23:56:26 +0200
Subject: [PATCH] Base of Svelte, and Back management.

---
 astro/services/Competition.go                |  81 ++----
 astro/services/Person.go                     |  21 +-
 astro/services/Session.go                    |  59 +++-
 astro/services/Stage.go                      |   1 +
 frontend/index.html                          |  27 +-
 frontend/package-lock.json                   |  25 ++
 frontend/package.json                        |   9 +-
 frontend/public/style.css                    |  67 +----
 frontend/src/App.svelte                      |  27 +-
 frontend/src/components/NavBar.svelte        |  94 +++++++
 frontend/src/components/Registrations.svelte | 268 +++++++++++++++++++
 main.go                                      |   9 +-
 12 files changed, 514 insertions(+), 174 deletions(-)
 create mode 100644 frontend/src/components/NavBar.svelte
 create mode 100644 frontend/src/components/Registrations.svelte

diff --git a/astro/services/Competition.go b/astro/services/Competition.go
index 4c85011..6af2f67 100644
--- a/astro/services/Competition.go
+++ b/astro/services/Competition.go
@@ -1,7 +1,5 @@
 package services
 
-import "math"
-
 // Competition : Competition details
 type Competition struct {
 	CompetitionID             uint8 // 255 competitions max
@@ -10,8 +8,8 @@ type Competition struct {
 	CompetitionWeapon         Weapon
 	CompetitionState          State
 	CompetitionMaxStageNumber uint8
-	CompetitionStages         []Stage
-	CompetitionPlayers        []Player
+	CompetitionStages         map[uint8]*Stage
+	CompetitionPlayers        map[uint16]*Player
 }
 
 func (c *Competition) String() string {
@@ -27,50 +25,12 @@ func CreateCompetition(competitionID uint8, competitionName string, competitionC
 	c.CompetitionWeapon = competitionWeapon
 	c.CompetitionState = REGISTERING
 	c.CompetitionMaxStageNumber = competitionMaxStageNumber
-	c.CompetitionStages = []Stage{}
-	c.CompetitionPlayers = []Player{}
+	c.CompetitionStages = map[uint8]*Stage{}
+	c.CompetitionPlayers = map[uint16]*Player{}
 
 	return c
 }
 
-func (c *Competition) AddStage(stage Stage) bool {
-	if c.CompetitionState != IDLE {
-		return false
-	}
-
-	if len(c.CompetitionStages) >= int(c.CompetitionMaxStageNumber) {
-		return false
-	}
-
-	c.CompetitionStages = append(c.CompetitionStages, stage)
-
-	return true
-}
-
-func (c *Competition) StagePosition(stage Stage) uint16 {
-	for i, competitionStage := range c.CompetitionStages {
-		if competitionStage == stage {
-			return uint16(i)
-		}
-	}
-
-	return math.MaxInt16
-}
-
-func (c *Competition) RemoveStage(stage Stage) bool {
-	if c.CompetitionState != IDLE {
-		return false
-	}
-
-	if c.StagePosition(stage) == math.MaxInt16 {
-		return false
-	}
-
-	c.CompetitionStages = append(c.CompetitionStages[:c.StagePosition(stage)], c.CompetitionStages[c.StagePosition(stage)+1:]...)
-
-	return true
-}
-
 func (c *Competition) StartCompetition() bool {
 	if c.CompetitionState != REGISTERING {
 		return false
@@ -93,38 +53,26 @@ func (c *Competition) FinishCompetition() bool {
 	return true
 }
 
-func (c *Competition) AddPlayer(player Player) bool {
+func (c *Competition) AddPlayer(player *Player) bool {
 	if c.CompetitionState != REGISTERING {
 		return false
 	}
 
-	c.CompetitionPlayers = append(c.CompetitionPlayers, player)
+	c.CompetitionPlayers[player.PlayerID] = player
 
 	return true
 }
 
-func (c *Competition) PlayerPosition(player Player) uint16 {
-	for i, competitionPlayer := range c.CompetitionPlayers {
-		if competitionPlayer == player {
-			return uint16(i)
-		}
-	}
-
-	return math.MaxInt16
-}
-
-func (c *Competition) RemovePlayer(player Player) bool {
+func (c *Competition) RemovePlayer(player *Player) bool {
 	if c.CompetitionState != REGISTERING {
 		return false
 	}
 
-	pos := c.PlayerPosition(player)
-
-	if pos == math.MaxInt16 {
+	if _, ok := c.CompetitionPlayers[player.PlayerID]; !ok {
 		return false
 	}
 
-	c.CompetitionPlayers = append(c.CompetitionPlayers[:pos], c.CompetitionPlayers[pos+1:]...)
+	delete(c.CompetitionPlayers, player.PlayerID)
 
 	return true
 }
@@ -136,3 +84,14 @@ func (c *Competition) AddPlayerToStage(player Player, stage Stage) bool {
 func (c *Competition) RemovePlayerFromStage(player Player, stage Stage) bool {
 	return stage.RemovePlayer(player)
 }
+
+func (c *Competition) UpdatePlayer(player *Player) bool {
+	// Check if player is in competition
+	if _, ok := c.CompetitionPlayers[player.PlayerID]; !ok {
+		return false
+	}
+
+	c.CompetitionPlayers[player.PlayerID] = player
+
+	return true
+}
diff --git a/astro/services/Person.go b/astro/services/Person.go
index 4161d08..8233f4c 100644
--- a/astro/services/Person.go
+++ b/astro/services/Person.go
@@ -5,9 +5,9 @@ type Player struct {
 	PlayerID          uint16 // More than 255 players
 	PlayerFirstname   string
 	PlayerLastname    string
-	PlayerNationID    uint16
-	PlayerRegionID    uint16
-	PlayerClubID      uint16
+	PlayerNation      *Nation
+	PlayerRegion      *Region
+	PlayerClub        *Club
 	PlayerInitialRank uint16
 }
 
@@ -15,15 +15,12 @@ func (p Player) String() string {
 	return "Player : " + p.PlayerFirstname + " " + p.PlayerLastname
 }
 
-func CreatePlayer(playerID uint16, playerFirstname string, playerLastname string) Player {
-	var p Player
-
-	p.PlayerID = playerID
-	p.PlayerFirstname = playerFirstname
-	p.PlayerLastname = playerLastname
-	p.PlayerInitialRank = 10000
-
-	return p
+func CreatePlayer(playerID uint16, playerFirstname string, playerLastname string) *Player {
+	return &Player{
+		PlayerID:        playerID,
+		PlayerFirstname: playerFirstname,
+		PlayerLastname:  playerLastname,
+	}
 }
 
 // Referee : Person details
diff --git a/astro/services/Session.go b/astro/services/Session.go
index 2d6fae9..b673da6 100644
--- a/astro/services/Session.go
+++ b/astro/services/Session.go
@@ -4,7 +4,7 @@ const MinStageSize = 3
 
 // Session : Session details
 type Session struct {
-	Competitions      []Competition
+	Competitions      [](*Competition)
 	CompetitionNumber uint8
 }
 
@@ -16,7 +16,7 @@ func (s *Session) AddCompetition(name string, category string, weapon string) {
 		CreateWeapon(weapon),
 		MinStageSize)
 
-	s.Competitions = append(s.Competitions, competition)
+	s.Competitions = append(s.Competitions, &competition)
 	s.CompetitionNumber++
 }
 
@@ -30,28 +30,49 @@ func (s *Session) RemoveCompetition(competitionID uint8) {
 }
 
 func (s *Session) GetCompetitions() []Competition {
-	return s.Competitions
+	comps := []Competition{}
+	for _, competition := range s.Competitions {
+		comps = append(comps, *competition)
+	}
+	return comps
 }
 
-func (s *Session) GetCompetition(competitionID uint8) Competition {
+func (s *Session) GetCompetition(competitionID uint8) *Competition {
 	for _, competition := range s.Competitions {
 		if competition.CompetitionID == competitionID {
 			return competition
 		}
 	}
-	return Competition{}
+	return nil
 }
 
-func (s *Session) AddPlayerToCompetition(competitionID uint8, player Player) bool {
+func (s *Session) AddPlayerToCompetition(competitionID uint8, player *Player) bool {
 	competition := s.GetCompetition(competitionID)
-	if competition.CompetitionName == "" {
+	if competition == nil {
+		return false
+	}
+
+	if player == nil {
 		return false
 	}
 
+	// If player id is UINT16MAX, set it
+	var id uint16 = 0
+	for {
+		// Check if id is already taken
+		_, ok := competition.CompetitionPlayers[id]
+		if !ok {
+			break
+		}
+		id++
+	}
+
+	player.PlayerID = id
+
 	return competition.AddPlayer(player)
 }
 
-func (s *Session) RemovePlayerFromCompetition(competitionID uint8, player Player) bool {
+func (s *Session) RemovePlayerFromCompetition(competitionID uint8, player *Player) bool {
 	competition := s.GetCompetition(competitionID)
 	if competition.CompetitionName == "" {
 		return false
@@ -59,3 +80,25 @@ func (s *Session) RemovePlayerFromCompetition(competitionID uint8, player Player
 
 	return competition.RemovePlayer(player)
 }
+
+func (s *Session) GetAllPlayersFromCompetition(competitionID uint8) []*Player {
+	competition := s.GetCompetition(competitionID)
+	if competition.CompetitionName == "" {
+		return []*Player{}
+	}
+
+	players := []*Player{}
+	for _, player := range competition.CompetitionPlayers {
+		players = append(players, player)
+	}
+	return players
+}
+
+func (s *Session) UpdateCompetitionPlayer(competitionID uint8, player *Player) bool {
+	competition := s.GetCompetition(competitionID)
+	if competition.CompetitionName == "" {
+		return false
+	}
+
+	return competition.UpdatePlayer(player)
+}
diff --git a/astro/services/Stage.go b/astro/services/Stage.go
index a222597..1360124 100644
--- a/astro/services/Stage.go
+++ b/astro/services/Stage.go
@@ -6,4 +6,5 @@ type Stage interface {
 	PlayerPosition(player Player) uint16
 	AddPlayer(player Player) bool
 	RemovePlayer(player Player) bool
+	GetID() uint8
 }
diff --git a/frontend/index.html b/frontend/index.html
index 4a3d896..11475b3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,14 +1,17 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/wails.png" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <link rel="stylesheet" href="/style.css" />
-    <title>AstroProject</title>
-  </head>
-  <body>
-    <div id="app"></div>
-    <script type="module" src="/src/main.ts"></script>
-  </body>
-</html>
+
+<head>
+  <meta charset="UTF-8" />
+  <link rel="icon" type="image/svg+xml" href="/wails.png" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <link rel="stylesheet" href="/style.css" />
+  <title>AstroProject</title>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c9ab675..6fecb76 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -7,6 +7,9 @@
     "": {
       "name": "frontend",
       "version": "0.0.0",
+      "dependencies": {
+        "sweetalert": "^2.1.2"
+      },
       "devDependencies": {
         "@sveltejs/vite-plugin-svelte": "^3.0.1",
         "@tsconfig/svelte": "^5.0.2",
@@ -981,6 +984,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/es6-object-assign": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
+      "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==",
+      "license": "MIT"
+    },
     "node_modules/es6-promise": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@@ -1415,6 +1424,12 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/promise-polyfill": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz",
+      "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==",
+      "license": "MIT"
+    },
     "node_modules/readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1666,6 +1681,16 @@
         }
       }
     },
+    "node_modules/sweetalert": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/sweetalert/-/sweetalert-2.1.2.tgz",
+      "integrity": "sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==",
+      "license": "MIT",
+      "dependencies": {
+        "es6-object-assign": "^1.1.0",
+        "promise-polyfill": "^6.0.2"
+      }
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 231af17..0122387 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,11 +12,14 @@
   "devDependencies": {
     "@sveltejs/vite-plugin-svelte": "^3.0.1",
     "@tsconfig/svelte": "^5.0.2",
+    "@wailsio/runtime": "latest",
     "svelte": "^4.2.8",
     "svelte-check": "^3.6.2",
     "tslib": "^2.6.2",
     "typescript": "^5.2.2",
-    "vite": "^5.0.8",
-    "@wailsio/runtime": "latest"
+    "vite": "^5.0.8"
+  },
+  "dependencies": {
+    "sweetalert": "^2.1.2"
   }
-}
\ No newline at end of file
+}
diff --git a/frontend/public/style.css b/frontend/public/style.css
index 4ef5666..7a1be50 100644
--- a/frontend/public/style.css
+++ b/frontend/public/style.css
@@ -1,7 +1,7 @@
 :root {
     font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
-    "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
-    sans-serif;
+        "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
+        "Helvetica Neue", sans-serif;
     font-size: 16px;
     line-height: 24px;
     font-weight: 400;
@@ -26,8 +26,7 @@
     font-family: "Inter";
     font-style: normal;
     font-weight: 400;
-    src: local(""),
-    url("./Inter-Medium.ttf") format("truetype");
+    src: local(""), url("./Inter-Medium.ttf") format("truetype");
 }
 
 h3 {
@@ -45,17 +44,6 @@ a:hover {
     color: #535bf2;
 }
 
-button {
-    width: 60px;
-    height: 30px;
-    line-height: 30px;
-    border-radius: 3px;
-    border: none;
-    margin: 0 0 0 20px;
-    padding: 0 8px;
-    cursor: pointer;
-}
-
 .result {
     height: 20px;
     line-height: 20px;
@@ -63,60 +51,14 @@ button {
 
 body {
     margin: 0;
-    display: flex;
-    place-items: center;
-    place-content: center;
-    min-width: 320px;
     min-height: 100vh;
 }
 
-.container {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-}
-
 h1 {
     font-size: 3.2em;
     line-height: 1.1;
 }
 
-#app {
-    max-width: 1280px;
-    margin: 0 auto;
-    padding: 2rem;
-    text-align: center;
-}
-
-.logo {
-    height: 6em;
-    padding: 1.5em;
-    will-change: filter;
-}
-
-.logo:hover {
-    filter: drop-shadow(0 0 2em #e80000aa);
-}
-
-.logo.vanilla:hover {
-    filter: drop-shadow(0 0 2em #f7df1eaa);
-}
-
-.result {
-    height: 20px;
-    line-height: 20px;
-    margin: 1.5rem auto;
-    text-align: center;
-}
-
-.footer {
-    margin-top: 1rem;
-    align-content: center;
-    text-align: center;
-    color: rgba(255, 255, 255, 0.67);
-}
-
 @media (prefers-color-scheme: light) {
     :root {
         color: #213547;
@@ -132,7 +74,6 @@ h1 {
     }
 }
 
-
 .input-box .btn:hover {
     background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
     color: #333333;
@@ -158,4 +99,4 @@ h1 {
 .input-box .input:focus {
     border: none;
     background-color: rgba(255, 255, 255, 1);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index 634fb3f..9e86756 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -1,24 +1,25 @@
 <script lang="ts">
-    import { Competition } from "../bindings/changeme/astro/services/models";
+    import { onMount } from "svelte";
+
+    import NavBar from "./components/NavBar.svelte";
+    import Registrations from "./components/Registrations.svelte";
     import * as Session from "../bindings/changeme/astro/services/session";
+    import { Competition } from "../bindings/changeme/astro/services/models";
 
     let competitions: Competition[] = [];
 
-    function getCompetitions() {
-        Session.GetCompetitions().then((data) => {
-            competitions = data;
-        });
-    }
+    onMount(async () => {
+        competitions = await Session.GetCompetitions();
+    });
 </script>
 
 <div class="container">
-    <h1>Competitions</h1>
-    <button on:click={getCompetitions}>Get</button>
-    <ul>
-        {#each competitions as competition}
-            <li>{competition.CompetitionName}</li>
-        {/each}
-    </ul>
+    <NavBar />
+    {#if competitions.length === 0}
+        <p>Loading...</p>
+    {:else}
+        <Registrations competition={competitions[0]} />
+    {/if}
 </div>
 
 <style>
diff --git a/frontend/src/components/NavBar.svelte b/frontend/src/components/NavBar.svelte
new file mode 100644
index 0000000..84c48c7
--- /dev/null
+++ b/frontend/src/components/NavBar.svelte
@@ -0,0 +1,94 @@
+<script lang="ts">
+    import { Competition } from "../../bindings/changeme/astro/services/models";
+    import { randomColor, rightBrighness } from "../Util";
+    import {
+        GetCompetitions,
+        AddCompetition,
+        RemoveCompetition,
+    } from "../../bindings/changeme/astro/services/session";
+    import { onMount } from "svelte";
+
+    let competitions: Competition[] = [];
+
+    async function loadCompetitions() {
+        competitions = await GetCompetitions();
+    }
+
+    onMount(loadCompetitions);
+</script>
+
+<div class="nav-bar">
+    <div class="competition-container">
+        {#each competitions as competition}
+            <div
+                class="competition-el"
+                style="background-color: {randomColor(rightBrighness)};"
+            >
+                <span>{competition.CompetitionName}</span>
+                <button
+                    on:click={async () => {
+                        await RemoveCompetition(competition.CompetitionID);
+                        loadCompetitions();
+                    }}>X</button
+                >
+            </div>
+        {/each}
+    </div>
+    <div class="competition-modifier">
+        <button
+            on:click={async () => {
+                const newCompetition = await AddCompetition(
+                    "Nouvelle compet",
+                    "U20",
+                    "Foil"
+                );
+                loadCompetitions();
+            }}>Add competition</button
+        >
+    </div>
+</div>
+
+<style>
+    .nav-bar {
+        padding: 50px 10px 25px;
+        display: grid;
+        /* 70% for competitions, 30% for button */
+        grid-template-columns: 70% 30%;
+        gap: 10px;
+        border-bottom: solid #003566 3px;
+    }
+
+    .competition-container {
+        display: grid;
+        overflow-x: scroll;
+        grid-auto-flow: column;
+        gap: 10px;
+    }
+
+    .competition-modifier {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+    }
+
+    button {
+        padding: 10px;
+        border: none;
+        border-radius: 5px;
+        background-color: #003566;
+        color: white;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+    }
+
+    .competition-el {
+        min-width: 20vw;
+        display: flex;
+        justify-content: space-between;
+        padding: 10px;
+        border: 1px solid #ccc;
+        border-radius: 5px;
+    }
+</style>
diff --git a/frontend/src/components/Registrations.svelte b/frontend/src/components/Registrations.svelte
new file mode 100644
index 0000000..6e53d03
--- /dev/null
+++ b/frontend/src/components/Registrations.svelte
@@ -0,0 +1,268 @@
+<script lang="ts">
+    import { onMount } from "svelte";
+    import * as Models from "../../bindings/changeme/astro/services/models";
+    import * as Session from "../../bindings/changeme/astro/services/session";
+    import swal from "sweetalert";
+
+    export let competition: Models.Competition;
+
+    let players: (Models.Player | null)[] = [];
+    let filteredPlayers: (Models.Player | null)[] = [];
+
+    async function loadPlayers() {
+        Session.GetAllPlayersFromCompetition(competition.CompetitionID).then(
+            (result) => {
+                players = result;
+            }
+        );
+    }
+
+    onMount(async () => {
+        await loadPlayers();
+    });
+
+    function getPlayerValueByKey(player: Models.Player | null, key: string) {
+        if (player === null) {
+            return "";
+        }
+
+        switch (key) {
+            case "PlayerInitialRank":
+                return player.PlayerInitialRank;
+            case "PlayerFirstname":
+                return player.PlayerFirstname;
+            case "PlayerLastname":
+                return player.PlayerLastname;
+            case "PlayerClub":
+                return player.PlayerClub;
+            default:
+                return "";
+        }
+    }
+
+    let sortBy = { key: "PlayerInitialRank", asc: true };
+
+    $: players = players.sort((a, b) => {
+        let aValue = getPlayerValueByKey(a, sortBy.key);
+        let bValue = getPlayerValueByKey(b, sortBy.key);
+        if (aValue === null || bValue === null) {
+            return 0;
+        }
+        if (aValue < bValue) {
+            return sortBy.asc ? -1 : 1;
+        }
+        if (aValue > bValue) {
+            return sortBy.asc ? 1 : -1;
+        }
+        return 0;
+    });
+
+    let searchTerms = "";
+
+    $: filteredPlayers = players.filter((player) => {
+        if (player === null) {
+            return [];
+        }
+
+        return (
+            player.PlayerFirstname.toLowerCase().includes(
+                searchTerms.toLowerCase()
+            ) ||
+            player.PlayerLastname.toLowerCase().includes(
+                searchTerms.toLowerCase()
+            )
+        );
+    });
+</script>
+
+<div class="registration-container">
+    <input
+        type="text"
+        bind:value={searchTerms}
+        placeholder="Search for a player"
+    />
+    <table>
+        <thead>
+            <tr>
+                <th
+                    on:click={() => {
+                        sortBy = { key: "PlayerInitialRank", asc: !sortBy.asc };
+                    }}>Initial Rank</th
+                >
+                <th
+                    on:click={() => {
+                        sortBy = { key: "PlayerFirstname", asc: !sortBy.asc };
+                    }}>Firstname</th
+                >
+                <th
+                    on:click={() => {
+                        sortBy = { key: "PlayerLastname", asc: !sortBy.asc };
+                    }}>Lastname</th
+                >
+                <th
+                    on:click={() => {
+                        sortBy = { key: "PlayerClub", asc: !sortBy.asc };
+                    }}>Club</th
+                >
+            </tr>
+        </thead>
+        <tbody>
+            {#each filteredPlayers as player}
+                {#if player != null}
+                    <tr>
+                        <td
+                            on:dblclick={async () => {
+                                let newRank = await swal({
+                                    content: {
+                                        element: "input",
+                                        attributes: {
+                                            placeholder: "New rank",
+                                            type: "number",
+                                        },
+                                    },
+                                    buttons: {
+                                        cancel: true,
+                                        confirm: true,
+                                    },
+                                });
+
+                                if (newRank) {
+                                    player.PlayerInitialRank =
+                                        parseInt(newRank);
+                                    await Session.UpdateCompetitionPlayer(
+                                        competition.CompetitionID,
+                                        player
+                                    ).then(() => {
+                                        loadPlayers();
+                                    });
+                                }
+                            }}>{player.PlayerInitialRank}</td
+                        >
+                        <td
+                            on:dblclick={async () => {
+                                let newFirstname = await swal({
+                                    content: {
+                                        element: "input",
+                                        attributes: {
+                                            placeholder: "New firstname",
+                                        },
+                                    },
+                                    buttons: {
+                                        cancel: true,
+                                        confirm: true,
+                                    },
+                                });
+
+                                if (newFirstname) {
+                                    player.PlayerFirstname = newFirstname;
+                                    await Session.UpdateCompetitionPlayer(
+                                        competition.CompetitionID,
+                                        player
+                                    ).then(() => {
+                                        loadPlayers();
+                                    });
+                                }
+                            }}>{player.PlayerFirstname}</td
+                        >
+                        <td
+                            on:dblclick={async () => {
+                                let newLastname = await swal({
+                                    content: {
+                                        element: "input",
+                                        attributes: {
+                                            placeholder: "New lastname",
+                                        },
+                                    },
+                                    buttons: {
+                                        cancel: true,
+                                        confirm: true,
+                                    },
+                                });
+
+                                if (newLastname) {
+                                    player.PlayerLastname = newLastname;
+                                    await Session.UpdateCompetitionPlayer(
+                                        competition.CompetitionID,
+                                        player
+                                    ).then(() => {
+                                        loadPlayers();
+                                    });
+                                }
+                            }}>{player.PlayerLastname}</td
+                        >
+                        <td
+                            >{#if player.PlayerClub}{player.PlayerClub}{/if}</td
+                        >
+                    </tr>
+                {/if}
+            {/each}
+            <tr id="add-player-tr">
+                <td colspan="4">
+                    <button
+                        on:click={async () => {
+                            var player = Models.Player.createFrom({
+                                PlayerID: 65535,
+                                PlayerFirstname: "Firstname",
+                                PlayerLastname: "Lastname",
+                                PlayerNationID: 0,
+                                PlayerClubID: 0,
+                                PlayerRegionID: 0,
+                                PlayerInitialRank: 2,
+                            });
+                            await Session.AddPlayerToCompetition(
+                                competition.CompetitionID,
+                                player
+                            ).then(() => {
+                                loadPlayers();
+                            });
+                        }}
+                    >
+                        Add Player
+                    </button></td
+                >
+            </tr>
+        </tbody>
+    </table>
+</div>
+
+<style>
+    .registration-container {
+        margin-top: 5vh;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+    }
+
+    input {
+        width: 200px;
+        margin-bottom: 10px;
+        padding: 10px;
+        font-size: 1em;
+        border-radius: 10px;
+        border: solid 1px #003566;
+    }
+
+    table {
+        width: 90%;
+        border-collapse: collapse;
+    }
+
+    th,
+    td {
+        padding: 8px;
+        text-align: left;
+    }
+
+    td {
+        border-bottom: 1px solid #e9ecef;
+    }
+
+    #add-player-tr td {
+        border: none;
+    }
+
+    th {
+        background-color: #f8f9fa;
+        cursor: pointer;
+    }
+</style>
diff --git a/main.go b/main.go
index 845d444..6cc102e 100644
--- a/main.go
+++ b/main.go
@@ -14,7 +14,11 @@ var assets embed.FS
 func main() {
 
 	session := services.Session{}
-	session.AddCompetition("Competition 1", "Category 1", "Weapon 1")
+	session.AddCompetition("Premier League", "U17", "Foil")
+	session.AddCompetition("Premier League", "U17", "Foil")
+	session.AddCompetition("Premier League", "U17", "Foil")
+	session.AddCompetition("Premier League", "U17", "Foil")
+	session.Competitions[0].AddPlayer(services.CreatePlayer(1, "John", "Doe"))
 
 	app := application.New(application.Options{
 		Name:        "AstroProject",
@@ -24,6 +28,7 @@ func main() {
 			application.NewService(&services.Competition{}),
 			application.NewService(&services.Club{}),
 			application.NewService(&services.Pool{}),
+			application.NewService(&services.Player{}),
 		},
 		Assets: application.AssetOptions{
 			Handler: application.AssetFileServerFS(assets),
@@ -42,7 +47,7 @@ func main() {
 		},
 		BackgroundColour: application.NewRGB(27, 38, 54),
 		URL:              "/",
-		Width:            500,
+		Width:            1080,
 		Height:           720,
 		Centered:         false,
 	})
-- 
GitLab