From 48bee368a286e11f1be6a80779413a91feece55c Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Fri, 29 Dec 2017 15:02:22 +0100 Subject: [PATCH] Drop PHP requiremeent: use localStorage --- .gitignore | 3 +- README.md | 21 +- css/index.css | 94 ++++---- doc/index.html | 24 +- doc/index.pug | 24 +- index.html | 7 +- joueurs.csv.dist | 1 - js/index.js | 220 ++++++++++-------- joueurs.sample.csv => participants.csv.sample | 14 +- scripts/rw_players.php | 42 ---- westcastle.sh | 2 - 11 files changed, 221 insertions(+), 231 deletions(-) delete mode 100644 joueurs.csv.dist rename joueurs.sample.csv => participants.csv.sample (65%) delete mode 100644 scripts/rw_players.php delete mode 100755 westcastle.sh diff --git a/.gitignore b/.gitignore index 524ce4b..ab6b7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ *.swp -/joueurs.csv -*.bak +*.csv .~lock* diff --git a/README.md b/README.md index 4654ca7..c62c1a3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,10 @@ -## Prérequis +## Liste des participants -php >= 5.4 - -## Ajustement du fichier de données - - 1. Renommer joueurs.csv.dist en joueurs.csv - 2. éditer joueurs.csv en s'inspirant de joueurs.sample.csv. Format en lignes : prénom,nom[,pdt,session,présent] - -pdt, session, présent : optionnels (par défaut resp. 0, 0, 1). - -pdt = "points de table" ; session = "mini-points" +Créer un fichier participants.csv en s'inspirant de participants.csv.sample +(À ne faire qu'une fois en début de saison / tournoi) ## Lancement de l'aplication - - [Linux] Double click sur "westcastle.sh", ou lancement depuis un terminal - - [Windows,MacOS] `php -S localhost:8000` puis aller à http://localhost:8000 dans un navigateur web - -## Utilisation +Naviguer vers index.html -Naviguer vers http://localhost:8000/doc +Pour l'aide, voir doc/index.html diff --git a/css/index.css b/css/index.css index 341c7b8..ece613b 100644 --- a/css/index.css +++ b/css/index.css @@ -50,6 +50,15 @@ img.logo { z-index: 10; } +.hide { + display: none; +} + +.clear { + clear: both; + overflow: auto; +} + .main { margin-left: 200px; /* Same as the width of the sidenav */ padding: 0px 10px; @@ -64,6 +73,7 @@ img.logo { font-size: 20px; padding: 10px 20px 10px 20px; text-decoration: none; + cursor: pointer; } .btn:hover { @@ -72,6 +82,11 @@ img.logo { text-decoration: none; } +button.block { + display: block; + margin: 25px auto; +} + table th { font-weight: bold; } @@ -82,7 +97,7 @@ table th { } .button-container-horizontal, .button-container-vertical { - margin-top: 30px; + margin: 15px 0; text-align: center; } .button-container-vertical { @@ -158,42 +173,6 @@ table.list tr:not(.title):hover, table.ranking tr:not(.title):nth-child(even):ho background-color: lightyellow; } -/* ranking div */ - -#ranking { - margin-bottom: 15px; - overflow: auto; -} - -table.ranking { - border-collapse: collapse; - width: 500px; - margin: 0 auto; - font-size: 1.1rem; - display: block; - float: left; -} - -table.ranking td -{ - border: 1px solid #ddd; - padding: 10px; -} -table.ranking th { - padding: 1em 10px; - text-align: left; -} - -table.ranking tr:not(.title) { - background-color: #aaa; -} -table.ranking tr:not(.title):nth-child(even){ - background-color: #ccc; -} -table.ranking tr:not(.title):hover, table.ranking tr:not(.title):nth-child(even):hover { - background-color: lightyellow; -} - /* pairings div */ .warning { @@ -206,11 +185,6 @@ span.link { cursor: pointer; } -button.block { - display: block; - margin: 25px auto; -} - .toto { color: darkgrey; } @@ -293,3 +267,39 @@ img.close-cross { right: 0; width: 30px; } + +/* ranking div */ + +#ranking { + margin-bottom: 15px; + overflow: auto; +} + +table.ranking { + border-collapse: collapse; + width: 500px; + margin: 0 auto; + font-size: 1.1rem; + display: block; + float: left; +} + +table.ranking td +{ + border: 1px solid #ddd; + padding: 10px; +} +table.ranking th { + padding: 1em 10px; + text-align: left; +} + +table.ranking tr:not(.title) { + background-color: #aaa; +} +table.ranking tr:not(.title):nth-child(even){ + background-color: #ccc; +} +table.ranking tr:not(.title):hover, table.ranking tr:not(.title):nth-child(even):hover { + background-color: lightyellow; +} diff --git a/doc/index.html b/doc/index.html index 7e870dc..8b4ad70 100644 --- a/doc/index.html +++ b/doc/index.html @@ -7,19 +7,19 @@ TL;DR:
    -
  1. Cliquer sur les joueurs absents dans l'onglet "joueurs"
  2. -
  3. Aller dans la section "appariements" et cliquer sur le bouton en haut
  4. -
  5. Lancer le chrono (section "chronomètre", puis click gauche [puis F11])
  6. -
  7. À la fin d'une ronde, cliquer sur chaque table pour indiquer les (mini-)points.
  8. +
  9. Cliquer sur les joueurs absents dans l'onglet "Participants"
  10. +
  11. Aller dans la section "appariements" et cliquer sur le bouton "Nouvelle ronde"
  12. +
  13. Lancer le chrono (section "Chronomètre", puis click gauche [puis F11])
  14. +
  15. Après la ronde, cliquer sur chaque table pour indiquer les (mini-)points, puis cliquer sur "Valider".
-

Le classement est mis à jour dans la rubrique correspondante et dans joueurs.csv.

+

Le classement est mis à jour dans la rubrique correspondante.

Voir aussi la vidéo de démonstration


L'application est divisée en 4 sections :

Participants

@@ -41,9 +41,8 @@ et la table s'affiche désormais sur fond vert. Un clic sur "Fermer" annule l'opération, aucun changement n'est effectué.

-

En cas d'erreur, ne surtout pas scorer une nouvelle table : d'abord restaurer l'état précédent en cliquant sur le bouton "Restaurer" - sur l'écran de classement ; alternativement, on peut cliquer sur le lien présent dans l'avertissement lorsqu'on reclique sur la table en question. - Ensuite, rentrer les scores corrigés. +

+ En cas d'erreur, re-cliquer sur la table à corriger : cliquer ensuite sur "Annuler", puis ré-entrer les scores.

Chronomètre

@@ -64,11 +63,10 @@

Classement

Les scores sont ordonnés par points de table décroissants d'abord, puis en cas d'ex-aequos par mini-points décroissants. + Ils peuvent être téléchargés via le bouton en haut à droite. Dans le fichier, "pdt" indique les points de table cumulés + et "session" les points de session cumulés (aussi appelés mini-points).

-

- Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois. - Cette action peut être annulée (immédiatement après) en cliquant sur "Restaurer". -

+

Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois.Attention cette action est irréversible.

\ No newline at end of file diff --git a/doc/index.pug b/doc/index.pug index 99aaef2..0662f42 100644 --- a/doc/index.pug +++ b/doc/index.pug @@ -10,11 +10,11 @@ html span.tldr TL;DR: ol - li Cliquer sur les joueurs absents dans l'onglet "joueurs" - li Aller dans la section "appariements" et cliquer sur le bouton en haut - li Lancer le chrono (section "chronomètre", puis click gauche [puis F11]) - li À la fin d'une ronde, cliquer sur chaque table pour indiquer les (mini-)points. - p Le classement est mis à jour dans la rubrique correspondante et dans joueurs.csv. + li Cliquer sur les joueurs absents dans l'onglet "Participants" + li Aller dans la section "appariements" et cliquer sur le bouton "Nouvelle ronde" + li Lancer le chrono (section "Chronomètre", puis click gauche [puis F11]) + li Après la ronde, cliquer sur chaque table pour indiquer les (mini-)points, #[strong puis cliquer sur "Valider".] + p Le classement est mis à jour dans la rubrique correspondante. p Voir aussi la #[a(href="https://auder.net/westcastle_demo.webm") vidéo de démonstration] @@ -25,7 +25,7 @@ html li Participants : la liste des joueurs présents (et absents) li Appariements : répartition des joueurs présents par tables li Chronomètre : un chrono qui démarre à 1h30 et sonne une fois à zéro - li Classement : le tableau des scores (voir aussi joueurs.csv) + li Classement : le tableau des scores h2 Participants @@ -47,9 +47,7 @@ html et la table s'affiche désormais sur fond vert. Un clic sur "Fermer" annule l'opération, aucun changement n'est effectué. p. - En cas d'erreur, #[strong ne surtout pas scorer une nouvelle table :] d'abord restaurer l'état précédent en cliquant sur le bouton "Restaurer" - sur l'écran de classement ; alternativement, on peut cliquer sur le lien présent dans l'avertissement lorsqu'on reclique sur la table en question. - Ensuite, rentrer les scores corrigés. + En cas d'erreur, re-cliquer sur la table à corriger : cliquer ensuite sur "Annuler", puis ré-entrer les scores. h2 Chronomètre @@ -69,7 +67,9 @@ html p. Les scores sont ordonnés par points de table décroissants d'abord, puis en cas d'ex-aequos par mini-points décroissants. + Ils peuvent être téléchargés via le bouton en haut à droite. Dans le fichier, "pdt" indique les points de table cumulés + et "session" les points de session cumulés (aussi appelés mini-points). - p. - Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois. - Cette action peut être annulée (immédiatement après) en cliquant sur "Restaurer". + p + | Un clic sur "Réinitialiser" remet tous les compteurs à zéro : par exemple pour démarrer un nouveau cycle de tournois. + strong Attention cette action est irréversible. diff --git a/index.html b/index.html index c84fd64..45a5ac7 100644 --- a/index.html +++ b/index.html @@ -20,16 +20,17 @@
- - + + - +
+ diff --git a/joueurs.csv.dist b/joueurs.csv.dist deleted file mode 100644 index d78ed64..0000000 --- a/joueurs.csv.dist +++ /dev/null @@ -1 +0,0 @@ -prenom,nom,pdt,session,present diff --git a/js/index.js b/js/index.js index f58a403..1dce8b6 100644 --- a/js/index.js +++ b/js/index.js @@ -6,7 +6,7 @@ new Vue({ }, components: { 'my-players': { - props: ['players'], + props: ['players','initPlayers'], template: `
@@ -27,6 +27,12 @@ new Vue({
+
+ + +
`, computed: { @@ -41,12 +47,22 @@ new Vue({ methods: { toggleAvailability: function(i) { this.players[i].available = 1 - this.players[i].available; - this.$forceUpdate(); //TODO (Vue.set... ?!) + }, + uploadTrigger: function() { + document.getElementById("upload").click(); + }, + upload: function(e) { + let file = (e.target.files || e.dataTransfer.files)[0]; + var reader = new FileReader(); + reader.onloadend = ev => { + this.initPlayers(ev.currentTarget.result); + }; + reader.readAsText(file); }, }, }, 'my-pairings': { - props: ['players','writeScoreToDb'], + props: ['players','commitScores'], data: function() { return { unpaired: [], @@ -59,7 +75,14 @@ new Vue({ template: `
- +
+ + +

Table {{ index+1 }}

@@ -83,18 +106,17 @@ new Vue({ {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }} - +
- - -
-
- Attention: un score a déjà été enregistré. - Les points indiqués ici s'ajouteront : il faut d'abord - restaurer l'état précédent. - Si c'est déjà fait, ignorer ce message :) + + +
@@ -152,36 +174,57 @@ new Vue({ this.sessions[index] = _.times(table.length, _.constant(0)); this.currentIndex = index; }, - setScore: function() { + closeScoreForm: function() { + if (!this.scored[this.currentIndex]) + this.sessions[this.currentIndex] = []; + this.currentIndex = -1; + }, + getPdts: function() { let sortedSessions = this.sessions[this.currentIndex] .map( (s,i) => { return {value:parseInt(s), index:i}; }) .sort( (a,b) => { return b.value - a.value; }); - let pdts = [4, 2, 1, 0]; + const ref_pdts = [4, 2, 1, 0]; // NOTE: take care of ex-aequos (spread points subtotal) let curSum = 0, curCount = 0, start = 0; + let sortedPdts = []; for (let i=0; i<4; i++) { - // Update pdts: - curSum += pdts[i]; + curSum += ref_pdts[i]; curCount++; if (i==3 || sortedSessions[i].value > sortedSessions[i+1].value) { let pdt = curSum / curCount; for (let j=start; j<=i; j++) - this.players[this.tables[this.currentIndex][sortedSessions[j].index]].pdt += pdt; + sortedPdts.push(pdt); curSum = 0; curCount = 0; start = i+1; } - // Update sessions: + } + // Re-order pdts to match table order + let pdts = [0, 0, 0, 0]; + for (let i=0; i<4; i++) + pdts[sortedSessions[i].index] = sortedPdts[i]; + return pdts; + }, + setScore: function() { + let pdts = this.getPdts(); + for (let i=0; i<4; i++) + { + this.players[this.tables[this.currentIndex][i]].pdt += pdts[i]; this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]); } - this.scored[this.currentIndex] = true; + Vue.set(this.scored, this.currentIndex, true); this.currentIndex = -1; - this.writeScoreToDb(); }, - clickRestore: function() { - document.getElementById('restoreBtn').click(); + resetScore: function() { + let pdts = this.getPdts(); + for (let i=0; i<4; i++) + { + this.players[this.tables[this.currentIndex][i]].pdt -= pdts[i]; + this.players[this.tables[this.currentIndex][i]].session -= parseInt(this.sessions[this.currentIndex][i]); + } + Vue.set(this.scored, this.currentIndex, false); }, }, }, @@ -249,7 +292,7 @@ new Vue({ }, }, 'my-ranking': { - props: ['players','sortByScore','writeScoreToDb'], + props: ['players','sortByScore','commitScores'], template: `
@@ -267,8 +310,11 @@ new Vue({
- - + + +
`, @@ -294,86 +340,78 @@ new Vue({ .sort(this.sortByScore); }, resetPlayers: function() { + if (confirm('Êtes-vous sûr ?')) + { + this.players + .slice(1) //discard Toto + .forEach( p => { + p.pdt = 0; + p.session = 0; + p.available = 1; + }); + this.commitScores(); + document.getElementById("doPairings").click(); + } + }, + download: function() { + // Prepare file content + let content = "prénom,nom,pdt,session\n"; this.players .slice(1) //discard Toto + .sort(this.sortByScore) .forEach( p => { - p.pdt = 0; - p.session = 0; - p.available = 1; + content += p.prenom + "," + p.nom + "," + p.pdt + "," + p.session + "\n"; }); - this.writeScoreToDb(); - document.getElementById("runPairing").click(); - }, - restoreLast: function() { - let xhr = new XMLHttpRequest(); - let self = this; - xhr.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) - { - let players = JSON.parse(xhr.responseText); - if (players.length > 0) - { - players.unshift({ //add ghost 4th player for 3-players tables - prenom: "Toto", - nom: "", - pdt: 0, - session: 0, - available: 0, - }); - // NOTE: Vue warning "do not mutate property" if direct self.players = players - for (let i=1; i { - p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0; - p.session = !!p.session ? parseInt(p.session) : 0; - p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func - }); - players.unshift({ //add ghost 4th player for 3-players tables - prenom: "Toto", - nom: "", - pdt: 0, - session: 0, - available: 0, - }); - self.players = players; - } - }; - xhr.open("GET", "scripts/rw_players.php", true); - xhr.send(null); + let players = JSON.parse(localStorage.getItem("players")); + if (players !== null) + { + this.addToto(players); + this.players = players; + } }, methods: { + addToto: function(array) { + array.unshift({ //add ghost 4th player for 3-players tables + prenom: "Toto", + nom: "", + pdt: 0, + session: 0, + available: 0, + }); + }, // Used both in ranking and pairings: sortByScore: function(a,b) { return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2; }, - writeScoreToDb: function() { - let xhr = new XMLHttpRequest(); - xhr.open("POST", "scripts/rw_players.php"); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - let orderedPlayers = this.players - .slice(1) //discard Toto - .sort(this.sortByScore); - xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers))); + commitScores: function() { + localStorage.setItem( + "players", + JSON.stringify(this.players.slice(1)) //discard Toto + ); + }, + // Used in players, reinit players array + initPlayers: function(csv) { + const allLines = csv + .split(/\r\n|\n|\r/) //line breaks + .splice(1); //discard header + let players = allLines + .filter( line => { return line.length > 0; }) //remove empty lines + .map( line => { + let parts = line.split(","); + return {prenom: parts[0], nom: parts[1], pdt: 0, session:0, available: 1}; + }); + this.addToto(players); + this.players = players; }, }, }); diff --git a/joueurs.sample.csv b/participants.csv.sample similarity index 65% rename from joueurs.sample.csv rename to participants.csv.sample index 5c62ea0..79e15d3 100644 --- a/joueurs.sample.csv +++ b/participants.csv.sample @@ -1,10 +1,10 @@ -prenom,nom,pdt,session,present -William,Plaisance,4,30,1 -Rosamonde,Dupuy,4,25,1 -Fabienne,Aubin,2,15,1 -Grégoire,Léveillé,2,10,0 -Fantina,Quinn,1,0,1 -Amitee,Morneau,1,-5,0 +prenom,nom +William,Plaisance +Rosamonde,Dupuy +Fabienne,Aubin +Grégoire,Léveillé +Fantina,Quinn +Amitee,Morneau William,Lespérance Dreux,Vadeboncoeur Brigliador,Cadieux diff --git a/scripts/rw_players.php b/scripts/rw_players.php deleted file mode 100644 index de63ad6..0000000 --- a/scripts/rw_players.php +++ /dev/null @@ -1,42 +0,0 @@ - $data[0], - "nom" => $data[1], - "pdt" => count($data)>=3 ? $data[2] : 0, - "session" => count($data)>=4 ? $data[3] : 0, - "available" => count($data)>=5 ? $data[4] : 1, - ); - $row++; - } - fclose($handle); - echo json_encode($players); -} -else -{ - copy("../joueurs.csv", "../joueurs.csv.bak"); //backup current checkpoint - // Write header + all players - $handle = fopen("../joueurs.csv", "w"); - fputcsv($handle, ["prenom","nom","pdt","session","present"]); - $players = json_decode($_POST["players"]); - foreach ($players as $p) - fputcsv($handle, (array)$p); - fclose($handle); -} - -?> diff --git a/westcastle.sh b/westcastle.sh deleted file mode 100755 index 37c479f..0000000 --- a/westcastle.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -(php -S localhost:8000 &) && sleep 1 && xdg-open http://localhost:8000 -- 2.44.0