From 48bee368a286e11f1be6a80779413a91feece55c Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
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&eacute;requis
+## Liste des participants
 
-php >= 5.4
-
-## Ajustement du fichier de donn&eacute;es
-
- 1. Renommer joueurs.csv.dist en joueurs.csv
- 2. &eacute;diter joueurs.csv en s'inspirant de joueurs.sample.csv. Format en lignes : pr&eacute;nom,nom[,pdt,session,pr&eacute;sent]
-
-pdt, session, pr&eacute;sent : optionnels (par d&eacute;faut resp. 0, 0, 1).
-
-pdt = "points de table" ; session = "mini-points"
+Cr&eacute;er un fichier participants.csv en s'inspirant de participants.csv.sample
+(&Agrave; ne faire qu'une fois en d&eacute;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 &agrave; 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 @@
   </head>
   <body><span class="tldr">TL;DR:</span>
     <ol>
-      <li>Cliquer sur les joueurs absents dans l'onglet "joueurs"</li>
-      <li>Aller dans la section "appariements" et cliquer sur le bouton en haut</li>
-      <li>Lancer le chrono (section "chronomètre", puis click gauche [puis F11])</li>
-      <li>À la fin d'une ronde, cliquer sur chaque table pour indiquer les (mini-)points.</li>
+      <li>Cliquer sur les joueurs absents dans l'onglet "Participants"</li>
+      <li>Aller dans la section "appariements" et cliquer sur le bouton "Nouvelle ronde"</li>
+      <li>Lancer le chrono (section "Chronomètre", puis click gauche [puis F11])</li>
+      <li>Après la ronde, cliquer sur chaque table pour indiquer les (mini-)points, <strong>puis cliquer sur "Valider".</strong></li>
     </ol>
-    <p>Le classement est mis à jour dans la rubrique correspondante et dans joueurs.csv.</p>
+    <p>Le classement est mis à jour dans la rubrique correspondante.</p>
     <p>Voir aussi la <a href="https://auder.net/westcastle_demo.webm">vidéo de démonstration</a></p>
     <hr><span>L'application est divisée en 4 sections :</span>
     <ul>
       <li>Participants : la liste des joueurs présents (et absents)</li>
       <li>Appariements : répartition des joueurs présents par tables</li>
       <li>Chronomètre : un chrono qui démarre à 1h30 et sonne une fois à zéro</li>
-      <li>Classement : le tableau des scores (voir aussi joueurs.csv)</li>
+      <li>Classement : le tableau des scores</li>
     </ul>
     <h2>Participants</h2>
     <p>
@@ -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é.
       
     </p>
-    <p>En cas d'erreur, <strong>ne surtout pas scorer une nouvelle table :</strong> 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.
+    <p>
+      En cas d'erreur, re-cliquer sur la table à corriger : cliquer ensuite sur "Annuler", puis ré-entrer les scores.
       
     </p>
     <h2>Chronomètre</h2>
@@ -64,11 +63,10 @@
     <h2>Classement</h2>
     <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>
-    <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>
+    <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.</strong></p>
   </body>
 </html>
\ 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 @@
 				<img class="logo" src="img/logo_Westcastle.png"/>
 			</header>
 			<main>
-				<my-players v-show="display=='players'" :players="players"></my-players>
-				<my-pairings v-show="display=='pairings'" :players="players" :write-score-to-db="writeScoreToDb"></my-pairings>
+				<my-players v-show="display=='players'" :players="players" :init-players="initPlayers"></my-players>
+				<my-pairings v-show="display=='pairings'" :players="players" :commit-scores="commitScores"></my-pairings>
 				<my-timer v-show="display=='timer'" @clockover="display='pairings'"></my-timer>
-				<my-ranking v-show="display=='ranking'" :players="players" :sort-by-score="sortByScore" :write-score-to-db="writeScoreToDb"></my-ranking>
+				<my-ranking v-show="display=='ranking'" :players="players" :sort-by-score="sortByScore" :commit-scores="commitScores"></my-ranking>
 			</main>
 		</div>
 	</body>
 
 	<script src="vendor/underscore-min.js"></script>
 	<script src="vendor/vue.min.js"></script>
+	<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>-->
 	<script src="js/index.js"></script>
 
 </html>
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: `
 				<div id="players">
 					<div class="left">
@@ -27,6 +27,12 @@ new Vue({
 							</tr>
 						</table>
 					</div>
+					<div class="clear">
+						<input class="hide" id="upload" type="file" @change="upload"/>
+						<button class="btn block cancel" @click="uploadTrigger()" title="Charge la liste des joueurs, en principe en début de tournoi">
+							(Ré)initialiser
+						</button>
+					</div>
 				</div>
 			`,
 			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: `
 				<div id="pairings">
 					<div v-show="currentIndex < 0">
-						<button id="runPairing" class="block btn" @click="doPairings()">Nouvelle ronde</button>
+						<div class="button-container-horizontal">
+							<button class="btn validate" :class="{hide: tables.length==0}" @click="commitScores()" title="Valide l'état actuel des scores (cf. rubrique Classement) en mémoire. Cette action est nécessaire au moins une fois en fin de tournoi après toutes les parties, et est recommandée après chaque ronde">
+								Valider
+							</button>
+							<button id="doPairings" class="btn" :class="{cancel: tables.length>0}" @click="doPairings()" title="Répartit les joueurs actifs aléatoirement sur les tables">
+								Nouvelle ronde
+							</button>
+						</div>
 						<div class="pairing" v-for="(table,index) in tables" :class="{scored: scored[index]}"
 								@click="showScoreForm(table,index)">
 							<p>Table {{ index+1 }}</p>
@@ -83,18 +106,17 @@ new Vue({
 								<td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
 									{{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
 								</td>
-								<td><input type="text" v-model="sessions[currentIndex][i]"/></td>
+								<td><input type="text" v-model="sessions[currentIndex][i]" :disabled="scored[currentIndex]"/></td>
 							</tr>
 						</table>
 						<div class="button-container-horizontal">
-							<button class="btn validate" @click="setScore()">Enregistrer</button>
-							<button class="btn" @click="currentIndex = -1">Fermer</button>
-						</div>
-						<div v-if="scored[currentIndex]" class="warning">
-							Attention: un score a déjà été enregistré.
-							Les points indiqués ici s'ajouteront : il faut d'abord
-							<span class="link" @click="clickRestore()">restaurer l'état précédent.</span>
-							Si c'est déjà fait, ignorer ce message :)
+							<button :class="{hide:scored[currentIndex]}" class="btn validate" @click="setScore()" title="Enregistre le score dans la base (la rubrique Classement est mise à jour)">
+								Enregistrer
+							</button>
+							<button :class="{hide:!scored[currentIndex]}" class="btn cancel" @click="resetScore()" title="Annule le score précédemment enregistré : l'action est visible dans la rubrique Classement">
+								Annuler
+							</button>
+							<button class="btn" @click="closeScoreForm()">Fermer</button>
 						</div>
 					</div>
 				</div>
@@ -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: `
 				<div id="ranking">
 					<table class="ranking">
@@ -267,8 +310,11 @@ new Vue({
 						</tr>
 					</table>
 					<div class="button-container-vertical" style="width:200px">
-						<button class="btn cancel" @click="resetPlayers()">Réinitialiser</button>
-						<button id="restoreBtn" class="btn" @click="restoreLast()">Restaurer</button>
+						<a id="download" href="#"></a>
+						<button class="btn" @click="download()" title="Télécharge le classement courant au format CSV">Télécharger</button>
+						<button class="btn cancel" @click="resetPlayers()" title="Réinitialise les scores à zéro. ATTENTION: action irréversible">
+							Réinitialiser
+						</button>
 					</div>
 				</div>
 			`,
@@ -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<players.length; i++)
-								{
-									players[i].pdt = parseFloat(players[i].pdt);
-									players[i].session = parseInt(players[i].session);
-									Vue.set(self.players, i, players[i]);
-								}
-							}
-						}
-					};
-					xhr.open("GET", "scripts/rw_players.php?restore=1", true);
-					xhr.send(null);
+					// Prepare and trigger download link
+					let downloadAnchor = document.getElementById("download");
+					downloadAnchor.setAttribute("download", "classement.csv");
+					downloadAnchor.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
+					downloadAnchor.click();
 				},
 			},
 		},
 	},
 	created: function() {
-		let xhr = new XMLHttpRequest();
-		let self = this;
-		xhr.onreadystatechange = function() {
-			if (this.readyState == 4 && this.status == 200)
-			{
-				let players = JSON.parse(xhr.responseText);
-				players.forEach( p => {
-					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 @@
-<?php
-
-if (!isset($_POST["players"]))
-{
-	if (isset($_GET["restore"]) && $_GET["restore"])
-	{
-		// Restore backup
-		if (!rename("../joueurs.csv.bak", "../joueurs.csv"))
-			exit("[]");
-	}
-	// Retrieve all players
-	$handle = fopen("../joueurs.csv", "r");
-	$players = [];
-	$row = 0;
-	$data = fgetcsv($handle); //skip header
-	while (($data = fgetcsv($handle)) !== FALSE)
-	{
-		$players[$row] = array(
-			"prenom" => $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