From: Benjamin Auder <benjamin.auder@somewhere> Date: Fri, 29 Dec 2017 14:02:22 +0000 (+0100) Subject: Drop PHP requiremeent: use localStorage X-Git-Url: https://git.auder.net/variants/Chakart/img/doc/css/config.php?a=commitdiff_plain;h=48bee368a286e11f1be6a80779413a91feece55c;p=westcastle.git Drop PHP requiremeent: use localStorage --- 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 @@ </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