aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKaran Jayachandra <me@karanj.com>2025-11-14 18:10:49 +0100
committerKaran Jayachandra <me@karanj.com>2025-11-14 18:10:49 +0100
commit202cf761e5162720f25521a75ae7cd6a4e0e5456 (patch)
tree0e57324bc72629cba3ed817b3439780f6959e509 /src
parent90390365bd93a4ee086c2931bb70ffa571a726b9 (diff)
parent56eb065a8165b3f508a35734adbb00c76bee012a (diff)
Merge branch 'develop' into 'main'
Develop See merge request KaranJayachandra/match_up!21
Diffstat (limited to 'src')
-rw-r--r--src/data.js98
-rw-r--r--src/main.js184
-rw-r--r--src/manage.js179
-rw-r--r--src/style.css12
-rw-r--r--src/timer.js84
5 files changed, 557 insertions, 0 deletions
diff --git a/src/data.js b/src/data.js
new file mode 100644
index 0000000..4358ee6
--- /dev/null
+++ b/src/data.js
@@ -0,0 +1,98 @@
+class Switch {
+ constructor(id, description, status) {
+ this.id = id;
+ this.status = typeof status !== "undefined" ? status : false;
+ this.description = description;
+ this.name = description + " " + (id + 1);
+ }
+ toggle() {
+ this.status = !this.status;
+ this.print();
+ }
+ print() {
+ let status = this.status ? "active" : "inactive";
+ console.log(this.name + " is currently " + status);
+ }
+ render() {
+ let color = this.status ? "is-success" : "is-danger";
+ let type = `class="button is-medium ${color}"`;
+ let id = this.description + "_" + this.id;
+ return `<button id=${id} ${type}>${this.name}</button>`;
+ }
+}
+
+export class Court extends Switch {
+ constructor(id, status) {
+ super(id, "Court", status);
+ }
+}
+
+export class Player extends Switch {
+ constructor(id, status, level, games, name) {
+ super(id, "Regular", status);
+ this.level = typeof level !== "undefined" ? level : 1;
+ this.games = typeof games !== "undefined" ? games : 0;
+ if (typeof name !== "undefined") {
+ this.name = name;
+ }
+ }
+}
+
+const guestCategories = {
+ 0: { description: "Beginner", level: 1 },
+ 1: { description: "Novice", level: 3 },
+ 2: { description: "Intermediate", level: 6 },
+};
+
+export class Guest extends Switch {
+ maxCount = 4;
+ categoryCount = 3;
+
+ constructor(id, status, games) {
+ super(id, "Guest", status);
+ const categoryId = Math.floor(id / this.maxCount) || 0;
+ this.level = guestCategories[categoryId].level;
+ this.games = typeof games !== "undefined" ? games : 0;
+ const guestNumber = (id % this.maxCount) + 1;
+ this.name = guestCategories[categoryId].description + " " + guestNumber;
+ }
+}
+
+export class Team {
+ constructor(playerOne, playerTwo) {
+ this.playerOne = playerOne;
+ this.playerTwo = playerTwo;
+ }
+ print() {
+ console.log("Team consists of:");
+ this.playerOne.print();
+ this.playerTwo.print();
+ }
+ render() {
+ let p1 = `<td style="width: 23%; text-align: right;">${this.playerOne}</td>`;
+ let p2 = `<td style="width: 23%; text-align: left;">${this.playerTwo}</td>`;
+ return p1 + p2;
+ }
+}
+
+export class Game extends Switch {
+ constructor(id, status, teamOne, teamTwo) {
+ super(id, "Game", status);
+ this.teamOne = teamOne;
+ this.teamTwo = teamTwo;
+ }
+ print() {
+ super.print();
+ let teamOne = this.teamOne.playerOne + " and " + this.teamOne.playerTwo;
+ let teamTwo = this.teamTwo.playerOne + " and " + this.teamTwo.playerTwo;
+ console.log(teamOne + " is playing against " + teamTwo);
+ }
+ render() {
+ let blockedState = this.teamOne.playerOne === "RESERVED";
+ let type = blockedState ? `class="is-danger is-dark"` : ``;
+ let court = `<td style="width: 8%; text-align: center;">${
+ this.id + 1
+ }</td>`;
+ return `<tr ${type}>${court}${this.teamOne.render()}${this.teamTwo.render()}</tr>`;
+ }
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..cb7ec8e
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,184 @@
+import "notyf/notyf.min.css";
+import "bulma/css/bulma.css";
+import "@fortawesome/fontawesome-free/css/all.css";
+import "./style.css";
+import { Notyf } from "notyf";
+import { Timer } from "./timer";
+import { Team, Game } from "./data";
+import { Manager, CourtManager, PlayerManager, GuestManager } from "./manage";
+
+export function getNotifier(seconds) {
+ return new Notyf({
+ duration: seconds * milliSecond,
+ position: { x: "center", y: "top" },
+ dismissible: true,
+ });
+}
+
+export class GameManager extends Manager {
+ timer = new Timer();
+ courts = new CourtManager();
+ players = new PlayerManager();
+ guests = new GuestManager();
+
+ constructor() {
+ super("Game", 12);
+ this.round = Number(localStorage.getItem("round")) || 0;
+ this.render();
+ addGameBehaviour(this);
+ }
+ createItem(i, description) {
+ if (typeof description === "undefined") {
+ description = "---";
+ }
+ let team = new Team(description, description);
+ return new Game(i, true, team, team);
+ }
+ loadItem(data) {
+ let id = Number(data.id);
+ let teamOne = new Team(data.teamOne.playerOne, data.teamOne.playerTwo);
+ let teamTwo = new Team(data.teamTwo.playerOne, data.teamTwo.playerTwo);
+ let status = Boolean(data.status);
+ return new Game(id, status, teamOne, teamTwo);
+ }
+ addBehaviour() {
+ let confirmedStatus = this.data[0].status;
+ let description = confirmedStatus ? "Round " + this.round : "Proposal";
+ document.getElementById("Game_description").innerHTML = description;
+ }
+ addBlockedGames() {
+ const inactiveCourts = this.courts.inactive();
+ console.log("Total reserved courts are " + inactiveCourts.length + ".");
+ for (let court of inactiveCourts) {
+ this.data.push(this.createItem(court.id, "RESERVED"));
+ }
+ }
+ addInactiveGames(courts) {
+ for (let court of courts) {
+ this.data.push(this.createItem(court.id));
+ }
+ }
+ addActiveGames(courts, players) {
+ for (let index = 0; index < courts.length; index++) {
+ const startIndex = 4 * index;
+ const endIndex = startIndex + 4;
+ const courtPlayers = players.slice(startIndex, endIndex);
+ const teamOne = new Team(courtPlayers[0].name, courtPlayers[3].name);
+ const teamTwo = new Team(courtPlayers[1].name, courtPlayers[2].name);
+ const game = new Game(courts[index].id, false, teamOne, teamTwo);
+ game.print();
+ this.data.push(game);
+ }
+ }
+ getPossibleGameCount() {
+ let activeRegularCount = this.players.active().length;
+ let activeGuestCount = this.guests.active().length;
+ let activePlayerCount = activeRegularCount + activeGuestCount;
+ console.log(
+ "There are a total of " + activePlayerCount + " players available."
+ );
+ let activeCourtCount = this.courts.active().length;
+ console.log(
+ "There are a total of " + activeCourtCount + " courts available."
+ );
+ let possibleMatches = Math.min(
+ Math.floor(activePlayerCount / 4),
+ activeCourtCount
+ );
+ return possibleMatches;
+ }
+ selectPlayers(count) {
+ console.log("Required players are " + count);
+ let activePlayers = this.players.active();
+ const activeGuests = this.guests.active();
+ activePlayers = activePlayers.concat(activeGuests);
+ console.log("Active players are:");
+ console.log(
+ activePlayers
+ .map((i) => {
+ return i.name;
+ })
+ .join(",")
+ );
+ const shuffledPlayers = shuffle(activePlayers);
+ shuffledPlayers.sort((a, b) => a.totalPlayed - b.totalPlayed);
+ const selectedPlayers = shuffledPlayers.slice(0, count);
+ selectedPlayers.sort((a, b) => a.level - b.level);
+ console.log("Selected players are:");
+ console.log(
+ selectedPlayers
+ .map((i) => {
+ return i.name;
+ })
+ .join(",")
+ );
+ return selectedPlayers;
+ }
+ propose() {
+ console.log("Proposing new games!");
+ let possibleMatches = this.getPossibleGameCount();
+ console.log("Total possible matches are " + possibleMatches + ".");
+ this.data = [];
+ this.addBlockedGames();
+ const activeCourts = this.courts.active();
+ const unusedCourts = activeCourts.slice(possibleMatches + 1);
+ this.addInactiveGames(unusedCourts);
+ const usedCourts = activeCourts.slice(0, possibleMatches);
+ let requiredPlayerCount = possibleMatches * 4;
+ const selectedPlayers = this.selectPlayers(requiredPlayerCount);
+ console.log("The matches are:");
+ this.addActiveGames(usedCourts, selectedPlayers);
+ this.data.sort((a, b) => a.id - b.id);
+ this.render();
+ }
+ confirm() {
+ let confirmedStatus = this.data[0].status;
+ if (confirmedStatus) {
+ getNotifier().error("Create a proposal first!");
+ return;
+ }
+ console.log("Confirm the proposal for round " + this.round + ".");
+ this.round += 1;
+ localStorage.setItem("round", this.round);
+ for (let i = 0; i < this.data.length; i++) {
+ this.data[i].status = true;
+ }
+ // Increment the number of games played
+ this.store();
+ this.render();
+ document.getElementById("timer-start").click();
+ }
+ reset() {
+ localStorage.removeItem("round");
+ localStorage.removeItem(this.description);
+ localStorage.removeItem(this.courts.description);
+ // Clear the number of games played
+ localStorage.removeItem(this.guests.description);
+ document.getElementById("timer-stop").click();
+ window.location.reload();
+ }
+}
+
+function shuffle(array) {
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array;
+}
+
+function addGameBehaviour(g) {
+ document.getElementById("propose").addEventListener("click", () => {
+ g.propose();
+ });
+ document.getElementById("confirm").addEventListener("click", () => {
+ g.confirm();
+ getNotifier().success("Round confirmed!");
+ });
+ document.getElementById("reset").addEventListener("click", () => {
+ g.reset();
+ getNotifier().error("Session has been reset!");
+ });
+}
+
+const g = new GameManager();
diff --git a/src/manage.js b/src/manage.js
new file mode 100644
index 0000000..952c72f
--- /dev/null
+++ b/src/manage.js
@@ -0,0 +1,179 @@
+import { Court, Guest, Player } from "./data";
+
+export class Manager {
+ constructor(description, maxCount) {
+ this.description = description;
+ this.maxCount = maxCount;
+ this.data = [];
+ this.count = this.data.length;
+ let data = JSON.parse(localStorage.getItem(this.description));
+ if (data === null || data.length === 0) {
+ this.generate();
+ return;
+ }
+ this.load(data);
+ }
+ generate() {
+ for (let i = 0; i < this.maxCount; i++) {
+ this.data.push(this.createItem(i));
+ }
+ console.log("Generated " + this.data.length + " " + this.description);
+ this.store();
+ this.render();
+ }
+ load(data) {
+ for (let i = 0; i < data.length; i++) {
+ this.data.push(this.loadItem(data[i]));
+ }
+ console.log("Loaded " + this.data.length + " " + this.description);
+ this.render();
+ }
+ store() {
+ let stringData = JSON.stringify(this.data);
+ localStorage.setItem(this.data[0].description, stringData);
+ }
+ render() {
+ let renderedHTML = this.data
+ .map((i) => {
+ return i.render();
+ })
+ .join("");
+ let elementId = this.data[0].description + "_list";
+ document.getElementById(elementId).innerHTML = renderedHTML;
+ this.addBehaviour();
+ }
+ addBehaviour() {
+ for (let idx = 0; idx < this.data.length; ++idx) {
+ const elementId = this.data[0].description + "_" + this.data[idx].id;
+ const p = document.getElementById(elementId);
+ p.addEventListener("click", () => {
+ this.data[idx].toggle();
+ this.store();
+ p.classList.toggle("is-success");
+ p.classList.toggle("is-danger");
+ });
+ }
+ }
+ active() {
+ return this.data.filter(function (c) {
+ return c.status;
+ });
+ }
+ inactive() {
+ return this.data.filter(function (c) {
+ return !c.status;
+ });
+ }
+}
+
+export class CourtManager extends Manager {
+ constructor() {
+ super("Court", 12);
+ }
+ createItem(i) {
+ return new Court(i, randomStatus());
+ }
+ loadItem(data) {
+ let id = Number(data.id);
+ let status = Boolean(Number(data.status));
+ return new Court(id, status);
+ }
+}
+
+export function randomStatus() {
+ return Math.random() < 0.5;
+}
+
+function normalize(data) {
+ let factor = Number(document.getElementById("skill").value);
+ console.log("Normalize skill by a factor of " + factor + ".");
+ return data.map(function (p) {
+ p.skill = Math.ceil(p.skill / factor);
+ return p;
+ });
+}
+
+export class GuestManager extends Manager {
+ constructor() {
+ super("Guest", 12);
+ }
+ createItem(i) {
+ return new Guest(i, randomStatus());
+ }
+ loadItem(data) {
+ let id = Number(data.id);
+ let status = Boolean(Number(data.status));
+ let games = data.games;
+ return new Guest(id, status, games);
+ }
+ active() {
+ return normalize(super.active());
+ }
+}
+
+export function randomLevel() {
+ const maxLevel = 10;
+ return Math.floor(Math.random() * maxLevel + 1);
+}
+
+export class PlayerManager extends Manager {
+ constructor() {
+ super("Regular", 71);
+ addLoadBehaviour(this);
+ }
+ createItem(i) {
+ return new Player(i, randomStatus(), randomLevel());
+ }
+ loadItem(data) {
+ let id = Number(data.id);
+ let name = data.name;
+ let level = Number(data.level);
+ let status = Boolean(Number(data.status));
+ let games = Number(data.games);
+ return new Player(id, status, level, games, name);
+ }
+ active() {
+ return normalize(super.active());
+ }
+}
+
+function parseLineToPlayer(data) {
+ var info = data.split(",");
+ var id = Number(info[0].trim());
+ var name = info[1].trim() + " " + info[2].trim();
+ var level = Number(info[3].trim());
+ return new Player(id, randomStatus(), level, 0, name);
+}
+
+function parseCsv(data) {
+ let lines = data.split("\n");
+ let players = [];
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i] == undefined || lines[i].trim() == "") {
+ continue;
+ }
+ players.push(parseLineToPlayer(lines[i]));
+ }
+ console.log("Loaded " + players.length + " Players");
+ for (let i = 0; i < players.length; i++) {
+ players[i].print();
+ }
+ return players;
+}
+
+function addLoadBehaviour(p) {
+ const f = document.querySelector("#player-load input[type=file]");
+ f.addEventListener("change", () => {
+ const file = f.files[0];
+ let importData;
+ console.log("Loading data from " + file.name);
+ const reader = new FileReader();
+ reader.onload = function (e) {
+ importData = reader.result;
+ p.data = parseCsv(importData);
+ p.store();
+ p.render();
+ };
+ reader.readAsText(file);
+ });
+}
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..1bd8901
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,12 @@
+.button-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ grid-gap: 15px;
+}
+#timer-display {
+ text-align: center;
+ width: fit-content;
+}
+table {
+ font-size: 1.5em;
+}
diff --git a/src/timer.js b/src/timer.js
new file mode 100644
index 0000000..c00ffdc
--- /dev/null
+++ b/src/timer.js
@@ -0,0 +1,84 @@
+const milliSecond = 1000;
+
+export class Timer {
+ pointer = 0;
+ mins = Number(sessionStorage.getItem("mins")) || 12;
+ running = Boolean(Number(sessionStorage.getItem("running"))) || false;
+ deadline = Number(sessionStorage.getItem("deadline")) || new Date().getTime();
+
+ constructor() {
+ this.running ? this.start(false) : this.reset();
+ addTimerBehaviour(this);
+ }
+ store() {
+ sessionStorage.setItem("mins", this.mins);
+ sessionStorage.setItem("running", Number(this.running));
+ sessionStorage.setItem("deadline", this.deadline);
+ }
+ set(m, s) {
+ const timer = document.getElementById("timer-display");
+ m = m < 10 ? "0" + m : m;
+ s = s < 10 ? "0" + s : s;
+ timer.value = `${m}:${s}`;
+ }
+ reset() {
+ clearInterval(this.pointer);
+ const mins = Math.floor(this.mins);
+ const secs = (this.mins - mins) * 60;
+ this.set(mins, secs);
+ this.running = false;
+ this.store();
+ }
+ update(ms) {
+ const m = Math.floor((ms % (milliSecond * 60 * 60)) / (milliSecond * 60));
+ const s = Math.floor((ms % (milliSecond * 60)) / milliSecond);
+ this.set(m, s);
+ }
+ refresh(p) {
+ const now = new Date().getTime();
+ const msToDeadline = p.deadline - now;
+ if (msToDeadline > 0) {
+ p.update(msToDeadline);
+ return;
+ }
+ p.reset();
+ getNotifier().error("Round completed");
+ }
+ start(fresh) {
+ if (fresh) {
+ const now = new Date().getTime();
+ const msToDeadline = this.mins * 60 * 1000;
+ this.deadline = now + msToDeadline;
+ }
+ this.running = true;
+ this.store();
+ this.pointer = setInterval(this.refresh, 1000, this);
+ }
+ change(value) {
+ this.mins += value;
+ if (this.mins < 0.5) {
+ this.mins = 0.5;
+ }
+ this.reset();
+ }
+}
+
+function addTimerBehaviour(t) {
+ document.getElementById("timer-start").addEventListener("click", () => {
+ if (t.running) return;
+ t.start(1);
+ });
+ document.getElementById("timer-stop").addEventListener("click", () => {
+ t.reset();
+ });
+ document.getElementById("timer-inc").addEventListener("click", () => {
+ if (!t.running) {
+ t.change(0.5);
+ }
+ });
+ document.getElementById("timer-dec").addEventListener("click", () => {
+ if (!t.running) {
+ t.change(-0.5);
+ }
+ });
+}