From 5b020e732156ee77d3b15b127aef2df57c2562ad Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Wed, 30 Jan 2019 21:10:14 +0100 Subject: [PATCH] Prepare new home --- client/next_src/views/Hall.vue | 176 ----------- .../views/correspondance_merge_hall.js | 161 ---------- client/src/main.js | 5 + client/src/playCompMove.js | 1 - client/src/router.js | 15 +- client/src/views/Home.vue | 297 +++++++++++++----- client/src/views/Variants.vue | 68 ++++ 7 files changed, 309 insertions(+), 414 deletions(-) delete mode 100644 client/next_src/views/Hall.vue delete mode 100644 client/next_src/views/correspondance_merge_hall.js create mode 100644 client/src/views/Variants.vue diff --git a/client/next_src/views/Hall.vue b/client/next_src/views/Hall.vue deleted file mode 100644 index c1ef7eb0..00000000 --- a/client/next_src/views/Hall.vue +++ /dev/null @@ -1,176 +0,0 @@ -<template> - <div class="home"> - <Home msg="Welcome to Your Vue.js Apppp"/> - </div> -</template> - -<script> -// @ is an alias to /src -import HelloWorld from "@/components/HelloWorld.vue"; - -export default { - name: "home", - components: { - HelloWorld, - } -}; -</script> - -// main playing hall: chat + online players + current challenges + button "new game" -// TODO: my-challenge-list, gérant clicks sur challenges, affichage, réception/émission des infos sur challenges ; de même, my-player-list -// TODO: si on est en train de jouer une partie, le notifier aux nouveaux connectés -/* -TODO: surligner si nouveau défi perso et pas affichage courant -(cadences base + incrément, corr == incr >= 1jour ou base >= 7j) ---> correspondance: stocker sur serveur lastMove + uid + color + movesCount + gameId + variant + timeleft -fin de partie corr: supprimer partie du serveur au bout de 7 jours (arbitraire) -main time should be positive (no 0+2 & cie...) -*/ -// TODO: au moins l'échange des coups en P2P ? -// TODO: objet game, objet challenge ? et player ? -Vue.component('my-room', { - props: ["conn","settings"], - data: function () { - return { - gdisplay: "live", - user: user, - liveGames: [], - corrGames: [], - players: [], //online players - challenges: [], //live challenges - people: [], //people who connect to this room (or disconnect) - }; - }, - // Modal new game, and then sub-components - template: ` - <div> - <input id="modalNewgame" type="checkbox" class="modal"/> - <div role="dialog" aria-labelledby="titleFenedit"> - <div class="card smallpad"> - <label id="closeNewgame" for="modalNewgame" class="modal-close"> - </label> - <h3 id="titleFenedit" class="section"> - {{ translate("Game state (FEN):") }} - </h3> - <input id="input-fen" type="text"/> - <p>TODO: cadence, adversaire (pre-filled if click on name)</p> - <p>cadence 2m+12s ou 7d+1d (m,s ou d,d) --> main, increment</p> - <p>Note: leave FEN blank for random; FEN only for targeted challenge</p> - <button @click="newGame">Launch game</button> - </div> - </div> - <div> - <my-chat :conn="conn" :myname="user.name" :people="people"></my-chat> - <my-challenge-list :challenges="challenges" @click-challenge="clickChallenge"> - </my-challenge-list> - </div> - <button onClick="doClick('modalNewgame')">New game</button> - <div> - <div style="border:1px solid black"> - <h3>Online players</h3> - <div v-for="p in players" @click="challenge(p)"> - {{ p.name }} - </div> - </div> - <div class="button-group"> - <button @click="gdisplay='live'">Live games</button> - <button @click="gdisplay='corr'">Correspondance games</button> - </div> - <my-game-list v-show="gdisplay=='live'" :games="liveGames" - @show-game="showGame"> - </my-game-list> - <my-game-list v-show="gdisplay=='corr'" :games="corrGames" - @show-game="showGame"> - </my-game-list> - </div> - </div> - `, - created: function() { - // TODO: ask server for current corr games (all but mines: names, ID, time control) - const socketMessageListener = msg => { - const data = JSON.parse(msg.data); - switch (data.code) - { - case "newgame": - // TODO: new game just started: data contain all informations - // (id, players, time control, fenStart ...) - break; - // TODO: also receive live games summaries (update) - // (just players names, time control, and ID + player ID) - case "acceptchallenge": - // oppid: opponent socket ID (or DB id if registered) - if (true) //TODO: if challenge is full - this.newGame(data.challenge, data.user); //user.id et user.name - break; - case "withdrawchallenge": - // TODO - break; - case "cancelchallenge": - // TODO - break; - // TODO: distinguish these (dis)connect events from their analogs in game.js - case "connect": - this.players.push({name:data.name, id:data.uid}); - break; - case "disconnect": - const pIdx = this.players.findIndex(p => p.id == data.uid); - this.players.splice(pIdx); - break; - } - }; - const socketCloseListener = () => { - this.conn.addEventListener('message', socketMessageListener); - this.conn.addEventListener('close', socketCloseListener); - }; - this.conn.onmessage = socketMessageListener; - this.conn.onclose = socketCloseListener; - }, - methods: { - translate: translate, - showGame: function(game) { - let hash = "#game?id=" + game.id; - if (!!game.uid) - hash += "&uid=" + game.uid; - location.hash = hash; - }, - challenge: function(player) { - this.conn.send(JSON.stringify({code:"sendchallenge", oppid:p.id, - user:{name:user.name,id:user.id}})); - }, - clickChallenge: function(challenge) { - const index = this.challenges.findIndex(c => c.id == challenge.id); - const toIdx = challenge.to.findIndex(p => p.id == user.id); - const me = {name:user.name,id:user.id}; - if (toIdx >= 0) - { - // It's a multiplayer challenge I accepted: withdraw - this.conn.send(JSON.stringify({code:"withdrawchallenge", - cid:challenge.id, user:me})); - this.challenges.to.splice(toIdx, 1); - } - else if (challenge.from.id == user.id) //it's my challenge: cancel it - { - this.conn.send(JSON.stringify({code:"cancelchallenge", cid:challenge.id})); - this.challenges.splice(index, 1); - } - else //accept a challenge - { - this.conn.send(JSON.stringify({code:"acceptchallenge", - cid:challenge.id, user:me})); - this.challenges[index].to.push(me); - } - }, - // user: last person to accept the challenge - newGame: function(chall, user) { - const fen = chall.fen || V.GenRandInitFen(); - const game = {}; //TODO: fen, players, time ... - //setStorage(game); //TODO - game.players.forEach(p => { - this.conn.send( - JSON.stringify({code:"newgame", oppid:p.id, game:game})); - }); - if (this.settings.sound >= 1) - new Audio("/sounds/newgame.mp3").play().catch(err => {}); - }, - }, -}); diff --git a/client/next_src/views/correspondance_merge_hall.js b/client/next_src/views/correspondance_merge_hall.js deleted file mode 100644 index 7dbde157..00000000 --- a/client/next_src/views/correspondance_merge_hall.js +++ /dev/null @@ -1,161 +0,0 @@ -Vue.component("my-correspondance", { - data: function() { - return { - userId: user.id, - games: [], - challenges: [], - willPlay: [], //IDs of challenges in which I decide to play (>= 3 players) - newgameInfo: { - fen: "", - vid: 0, - nbPlayers: 0, - players: [{id:0,name:""},{id:0,name:""},{id:0,name:""}], - mainTime: 0, - increment: 0, - }, - }; - }, - template: ` - <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> - <input id="modalNewgame" type="checkbox" class="modal"/> - <div role="dialog" aria-labelledby="titleFenedit"> - <div class="card smallpad"> - <label id="closeNewgame" for="modalNewgame" class="modal-close"> - </label> - <fieldset> - <label for="selectVariant">{{ translate("Variant") }}</label> - <select id="selectVariant" v-model="newgameInfo.vid"> - <option v-for="v in variants" :value="v.id"> - {{ v.name }} - </option> - </select> - </fieldset> - <fieldset> - <label for="selectNbPlayers"> - {{ translate("Number of players") }} - </label> - <select id="selectNbPlayers" v-model="newgameInfo.nbPlayers"> - <option v-show="possibleNbplayers(2)" value="2">2</option> - <option v-show="possibleNbplayers(3)" value="3">3</option> - <option v-show="possibleNbplayers(4)" value="4">4</option> - </select> - </fieldset> - <fieldset> - <label for="timeControl">Time control (in days)</label> - <div id="timeControl"> - <input type="number" v-model="newgameInfo.mainTime" - placeholder="Main time"/> - <input type="number" v-model="newgameInfo.increment" - placeholder="Increment"/> - </div> - </fieldset> - <fieldset> - <label for="selectPlayers">{{ translate("Play with?") }}</label> - <div id="selectPlayers"> - <input type="text" v-model="newgameInfo.players[0].name"/> - <input v-show="newgameInfo.nbPlayers>=3" type="text" - v-model="newgameInfo.players[1].name"/> - <input v-show="newgameInfo.nbPlayers==4" type="text" - v-model="newgameInfo.players[2].name"/> - </div> - </fieldset> - <fieldset> - <label for="inputFen"> - {{ translate("FEN (ignored if players fields are blank)") }} - </label> - <input id="inputFen" type="text" v-model="newgameInfo.fen"/> - </fieldset> - <button @click="newGame">Launch game</button> - </div> - </div> - <p v-if="!userId"> - Correspondance play is reserved to registered users - </p> - <div v-if="!!userId"> - <my-challenge-list :challenges="challenges" - @click-challenge="clickChallenge"> - </my-challenge-list> - <button onClick="doClick('modalNewgame')">New game</button> - <my-game-list :games="games" @show-game="showGame"> - </my-game-list> - </div> - </div> - `, - computed: { - // TODO: this is very artificial... - variants: function() { - return variantArray; - }, - }, - created: function() { - // use user.id to load challenges + games from server - }, - methods: { - translate: translate, - clickChallenge: function() { - // TODO: accepter un challenge peut lancer une partie, il - // faut alors supprimer challenge + creer partie + la retourner et l'ajouter ici - // autres actions: - // supprime mon défi - // accepte un défi - // annule l'acceptation d'un défi (si >= 3 joueurs) - // - // si pas le mien et FEN speciale :: (charger code variante et) - // montrer diagramme + couleur (orienté) - }, - showGame: function(g) { - // Redirect to /variant#game?id=... - location.href="/variant#game?id=" + g.id; - }, - newGame: function() { - const afterRulesAreLoaded = () => { - // NOTE: side-effect = set FEN - // TODO: (to avoid any cheating option) separate the GenRandInitFen() functions - // in separate files, load on server and generate FEN on server. - const error = checkChallenge(this.newgameInfo, vname); - if (!!error) - return alert(error); - // Possible (server) error if filled player does not exist - ajax( - "/challenges/" + this.newgameInfo.vid, - "POST", - this.newgameInfo, - response => { - const chall = Object.assign({}, - this.newgameInfo, - { - id: response.cid, - uid: user.id, - added: Date.now(), - vname: vname, - }, - this.challenges.push(response.challengei); - } - ); - }; - const idxInVariants = - variantArray.findIndex(v => v.id == this.newgameInfo.vid); - const vname = variantArray[idxInVariants].name; - const scriptId = vname + "RulesScript"; - if (!document.getElementById(scriptId)) - { - // Load variant rules (only once) - var script = document.createElement("script"); - script.id = scriptId; - script.onload = afterRulesAreLoaded; - //script.addEventListener ("load", afterRulesAreLoaded, false); - script.src = "/javascripts/variants/" + vname + ".js"; - document.body.appendChild(script); - } - else - afterRulesAreLoaded(); - }, - possibleNbplayers: function(nbp) { - if (this.newgameInfo.vid == 0) - return false; - const idxInVariants = - variantArray.findIndex(v => v.id == this.newgameInfo.vid); - return NbPlayers[variantArray[idxInVariants].name].includes(nbp); - }, - }, -}); diff --git a/client/src/main.js b/client/src/main.js index 46d99abe..97bad754 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -20,6 +20,11 @@ new Vue({ // }, created: function() { window.doClick = (elemId) => { document.getElementById(elemId).click() }; + + // TODO: AJAX call get corr games (all variants) + // si dernier lastMove sur serveur n'est pas le mien et nextColor == moi, alors background orange + // ==> background orange si à moi de jouer par corr (sur main index) + // (helper: static fonction "GetNextCol()" dans base_rules.js) //TODO: si une partie en cours dans storage, rediriger vers cette partie //(à condition que l'URL n'y corresponde pas déjà !) diff --git a/client/src/playCompMove.js b/client/src/playCompMove.js index 45da5113..d6b0cea9 100644 --- a/client/src/playCompMove.js +++ b/client/src/playCompMove.js @@ -5,7 +5,6 @@ onmessage = function(e) { case "scripts": self.importScripts( - '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js', '/javascripts/base_rules.js', '/javascripts/utils/array.js', '/javascripts/variants/' + e.data[1] + '.js'); diff --git a/client/src/router.js b/client/src/router.js index 7c2e2a68..b373cdc2 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -15,6 +15,16 @@ export default new Router({ name: "home", component: Home, }, + { + path: "/variants", + name: "variants", + component: loadView("Variants"), + }, +// { +// path: "/variants/:vname([a-zA-Z0-9]+)", +// name: "rules", +// component: Rules, +// }, // { // path: "/about", // name: "about", @@ -26,11 +36,6 @@ export default new Router({ // // return import(/* webpackChunkName: "about" */ "./views/About.vue"); // //} // }, -// { -// path: "/test", -// name: "test", -// component: loadView("Test"), -// }, // TODO: gameRef, problemId: https://router.vuejs.org/guide/essentials/dynamic-matching.html ] }); diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index dfd39391..c2c5b442 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -1,87 +1,242 @@ <template lang="pug"> div - .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 - label(for="prefixFilter") Type first letters... - input#prefixFilter(v-model="curPrefix") - .variant.col-sm-12.col-md-5.col-lg-4( - v-for="(v,idx) in sortedCounts" - :class="{'col-md-offset-1': idx%2==0, 'col-lg-offset-2': idx%2==0}" - ) - a(:href="v.name") - h4.boxtitle.text-center {{ v.name }} - span.count-players / {{ v.count }} - p.description.text-center {{ $tr(v.desc) }} + input#modalNewgame.modal(type="checkbox") + div(role="dialog" aria-labelledby="titleFenedit") + .card.smallpad + label#closeNewgame.modal-close(for="modalNewgame") + fieldset + label(for="selectVariant") {{ st.tr["Variant"] }} + select#selectVariant(v-model="newgameInfo.vid") + option(v-for="v in variants" :value="v.id") {{ v.name }} + fieldset + label(for="selectNbPlayers") {{ st.tr["Number of players"] }} + select#selectNbPlayers(v-model="newgameInfo.nbPlayers") + option(v-show="possibleNbplayers(2)" value="2") 2 + option(v-show="possibleNbplayers(3)" value="3") 3 + option(v-show="possibleNbplayers(4)" value="4") 4 + fieldset + label(for="timeControl") Time control (in days) + #timeControl + input(type="number" v-model="newgameInfo.mainTime" + placeholder="Main time") + input(type="number" v-model="newgameInfo.increment" + placeholder="Increment") + fieldset + label(for="selectPlayers") {{ st.tr["Play with?"] }} + #selectPlayers + input(type="text" v-model="newgameInfo.players[0].name") + input(v-show="newgameInfo.nbPlayers>=3" type="text" + v-model="newgameInfo.players[1].name") + input(v-show="newgameInfo.nbPlayers==4" type="text" + v-model="newgameInfo.players[2].name") + fieldset + label(for="inputFen") + {{ st.tr["FEN (ignored if players fields are blank)"] }} + input#inputFen(type="text" v-model="newgameInfo.fen") + button(@click="newGame") Launch game + p TODO: cadence, adversaire (pre-filled if click on name) + p cadence 2m+12s ou 7d+1d (m,s ou d,d) --> main, increment + p Note: leave FEN blank for random; FEN only for targeted challenge + div + my-challenge-list(:challenges="challenges" @click-challenge="clickChallenge") + div(style="border:1px solid black") + h3 Online players + div(v-for="p in players" @click="challenge(p)") {{ p.name }} + button(onClick="doClick('modalNewgame')") New game + div + .button-group + button(@click="gdisplay='live'") Live games + button(@click="gdisplay='corr'") Correspondance games + my-game-list(v-show="gdisplay=='live'" :games="liveGames" + @show-game="showGame") + my-game-list(v-show="gdisplay=='corr'" :games="corrGames" + @show-game="showGame") </template> <script> +// main playing hall: online players + current challenges + button "new game" +// TODO: si on est en train de jouer une partie, le notifier aux nouveaux connectés +/* +TODO: surligner si nouveau défi perso et pas affichage courant +(cadences base + incrément, corr == incr >= 1jour ou base >= 7j) +--> correspondance: stocker sur serveur lastMove + uid + color + movesCount + gameId + variant + timeleft +fin de partie corr: supprimer partie du serveur au bout de 7 jours (arbitraire) +*/ +// TODO: au moins l'échange des coups en P2P ? et game chat ? +// TODO: objet game, objet challenge ? et player ? +import { store } from "@/store"; +import GameList from "@/components/GameList.vue"; +import ChallengeList from "@/components/ChallengeList.vue"; export default { name: "home", - data: function() { - return { - counts: {}, - curPrefix: "", - }; - }, - computed: { - sortedCounts: function () { - const capitalizedPrefix = this.curPrefix.replace(/^\w/, c => c.toUpperCase()); - const variantsCounts = this.$variants - .filter( v => { - return v.name.startsWith(capitalizedPrefix); - }) - .map( v => { - return { - name: v.name, - desc: v.description, - count: this.counts[v.name] || 0, - }; - }); - return variantsCounts.sort((a,b) => { - if (a.count != b.count) - return b.count - a.count; - // Else, alphabetic ordering - return a.name.localeCompare(b.name); - }); - }, + components: { + GameList, + ChallengeList, + }, + data: function () { + return { + st: store.state, + gdisplay: "live", + liveGames: [], + corrGames: [], + players: [], //online players + challenges: [], //live challenges + willPlay: [], //IDs of challenges in which I decide to play (>= 3 players) + newgameInfo: { + fen: "", + vid: 0, + nbPlayers: 0, + players: [{id:0,name:""},{id:0,name:""},{id:0,name:""}], + mainTime: 0, + increment: 0, + }, + }; }, created: function() { + // TODO: ask server for current corr games (all but mines: names, ID, time control) const socketMessageListener = msg => { const data = JSON.parse(msg.data); - if (data.code == "counts") - this.counts = data.counts; - else if (data.code == "increase") - this.counts[data.vid]++; - else if (data.code == "decrease") - this.counts[data.vid]--; + switch (data.code) + { + case "newgame": + // TODO: new game just started: data contain all informations + // (id, players, time control, fenStart ...) + break; + // TODO: also receive live games summaries (update) + // (just players names, time control, and ID + player ID) + case "acceptchallenge": + // oppid: opponent socket ID (or DB id if registered) + if (true) //TODO: if challenge is full + this.newGame(data.challenge, data.user); //user.id et user.name + break; + case "withdrawchallenge": + // TODO + break; + case "cancelchallenge": + // TODO + break; + // TODO: distinguish these (dis)connect events from their analogs in game.js + case "connect": + this.players.push({name:data.name, id:data.uid}); + break; + case "disconnect": + const pIdx = this.players.findIndex(p => p.id == data.uid); + this.players.splice(pIdx); + break; + } }; const socketCloseListener = () => { - this.$conn.addEventListener('message', socketMessageListener); - this.$conn.addEventListener('close', socketCloseListener); + this.st.conn.addEventListener('message', socketMessageListener); + this.st.conn.addEventListener('close', socketCloseListener); }; - this.$conn.onmessage = socketMessageListener; - this.$conn.onclose = socketCloseListener; - // TODO: AJAX call get corr games (all variants) - // si dernier lastMove sur serveur n'est pas le mien et nextColor == moi, alors background orange - // ==> background orange si à moi de jouer par corr (sur main index) - // (helper: static fonction "GetNextCol()" dans base_rules.js) + this.st.conn.onmessage = socketMessageListener; + this.st.conn.onclose = socketCloseListener; + }, + methods: { + showGame: function(game) { + // NOTE: if we are an observer, the game will be found in main games list + // (sent by connected remote players) + this.$router.push("/" + game.id) + }, + challenge: function(player) { + this.st.conn.send(JSON.stringify({code:"sendchallenge", oppid:p.id, + user:{name:this.st.user.name,id:this.st.user.id}})); + }, + clickChallenge: function(challenge) { + const index = this.challenges.findIndex(c => c.id == challenge.id); + const toIdx = challenge.to.findIndex(p => p.id == user.id); + const me = {name:user.name,id:user.id}; + if (toIdx >= 0) + { + // It's a multiplayer challenge I accepted: withdraw + this.st.conn.send(JSON.stringify({code:"withdrawchallenge", + cid:challenge.id, user:me})); + this.challenges.to.splice(toIdx, 1); + } + else if (challenge.from.id == user.id) //it's my challenge: cancel it + { + this.st.conn.send(JSON.stringify({code:"cancelchallenge", cid:challenge.id})); + this.challenges.splice(index, 1); + } + else //accept a challenge + { + this.st.conn.send(JSON.stringify({code:"acceptchallenge", + cid:challenge.id, user:me})); + this.challenges[index].to.push(me); + } + // TODO: accepter un challenge peut lancer une partie, il + // faut alors supprimer challenge + creer partie + la retourner et l'ajouter ici + // autres actions: + // supprime mon défi + // accepte un défi + // annule l'acceptation d'un défi (si >= 3 joueurs) + // + // si pas le mien et FEN speciale :: (charger code variante et) + // montrer diagramme + couleur (orienté) + }, + // user: last person to accept the challenge + newGameLive: function(chall, user) { + const fen = chall.fen || V.GenRandInitFen(); + const game = {}; //TODO: fen, players, time ... + //setStorage(game); //TODO + game.players.forEach(p => { + this.conn.send( + JSON.stringify({code:"newgame", oppid:p.id, game:game})); + }); + if (this.settings.sound >= 1) + new Audio("/sounds/newgame.mp3").play().catch(err => {}); + }, + newGame: function() { + const afterRulesAreLoaded = () => { + // NOTE: side-effect = set FEN + // TODO: (to avoid any cheating option) separate the GenRandInitFen() functions + // in separate files, load on server and generate FEN on server. + const error = checkChallenge(this.newgameInfo, vname); + if (!!error) + return alert(error); + // Possible (server) error if filled player does not exist + ajax( + "/challenges/" + this.newgameInfo.vid, + "POST", + this.newgameInfo, + response => { + const chall = Object.assign({}, + this.newgameInfo, + { + id: response.cid, + uid: user.id, + added: Date.now(), + vname: vname, + }, + this.challenges.push(response.challenge); + } + ); + // TODO: else, if live game: send infos (socket), and... + }; + const idxInVariants = + variantArray.findIndex(v => v.id == this.newgameInfo.vid); + const vname = variantArray[idxInVariants].name; + const scriptId = vname + "RulesScript"; + if (!document.getElementById(scriptId)) + { + // Load variant rules (only once) + var script = document.createElement("script"); + script.id = scriptId; + script.onload = afterRulesAreLoaded; + //script.addEventListener ("load", afterRulesAreLoaded, false); + script.src = "/javascripts/variants/" + vname + ".js"; + document.body.appendChild(script); + } + else + afterRulesAreLoaded(); + }, + possibleNbplayers: function(nbp) { + if (this.newgameInfo.vid == 0) + return false; + const idxInVariants = + variantArray.findIndex(v => v.id == this.newgameInfo.vid); + return NbPlayers[variantArray[idxInVariants].name].includes(nbp); + }, }, -}; +}); </script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped lang="scss"> -h3 { - margin: 40px 0 0; -} -ul { - list-style-type: none; - padding: 0; -} -li { - display: inline-block; - margin: 0 10px; -} -a { - color: #42b983; -} -</style> diff --git a/client/src/views/Variants.vue b/client/src/views/Variants.vue new file mode 100644 index 00000000..03903572 --- /dev/null +++ b/client/src/views/Variants.vue @@ -0,0 +1,68 @@ +<template lang="pug"> +div + .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + label(for="prefixFilter") Type first letters... + input#prefixFilter(v-model="curPrefix") + .variant.col-sm-12.col-md-5.col-lg-4( + v-for="(v,idx) in filteredVariants" + :class="{'col-md-offset-1': idx%2==0, 'col-lg-offset-2': idx%2==0}" + ) + router-link(:to="getLink(v.name)") + h4.boxtitle.text-center {{ v.name }} + p.description.text-center {{ st.tr(v.desc) }} +</template> + +<script> +import { store } from "@/store"; +export default { + name: "variants", + data: function() { + return { + curPrefix: "", + st: store.state, + }; + }, + computed: { + filteredVariants: function () { + const capitalizedPrefix = this.curPrefix.replace(/^\w/, c => c.toUpperCase()); + const variants = this.st.variants + .filter( v => { + return v.name.startsWith(capitalizedPrefix); + }) + .map( v => { + return { + name: v.name, + desc: v.description, + }; + }) + .sort((a,b) => { + return a.name.localeCompare(b.name); + }); + return variants; + }, + }, + methods: { + getLink: function(vname) { + return "/variants/" + vname; + }, + }, +}; +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped lang="scss"> +h3 { + margin: 40px 0 0; +} +ul { + list-style-type: none; + padding: 0; +} +li { + display: inline-block; + margin: 0 10px; +} +a { + color: #42b983; +} +</style> -- 2.44.0