},
render(h) {
const [sizeX,sizeY] = [V.size.x,V.size.y];
- const smallScreen = (window.innerWidth <= 420);
// Precompute hints squares to facilitate rendering
let hintSquares = doubleArray(sizeX, sizeY, false);
this.possibleMoves.forEach(m => { hintSquares[m.end.x][m.end.y] = true; });
attrs: { "aria-label": 'New online game' },
'class': {
"tooltip": true,
+ "play": true,
"bottom": true, //display below
"seek": this.seek,
"playing": this.mode == "human",
- "small": smallScreen,
+ "spaceright": true,
},
},
[h('i', { 'class': { "material-icons": true } }, "accessibility")])
attrs: { "aria-label": 'New game VS computer' },
'class': {
"tooltip":true,
+ "play": true,
"bottom": true,
"playing": this.mode == "computer",
- "small": smallScreen,
+ "spaceright": true,
},
},
[h('i', { 'class': { "material-icons": true } }, "computer")])
attrs: { "aria-label": 'New IRL game' },
'class': {
"tooltip":true,
+ "play": true,
"bottom": true,
"playing": this.mode == "friend",
- "small": smallScreen,
+ "spaceright": true,
},
},
[h('i', { 'class': { "material-icons": true } }, "people")])
? parseFloat(window.getComputedStyle(square00).width.slice(0,-2))
: 0;
const settingsBtnElt = document.getElementById("settingsBtn");
- const indicWidth = !!settingsBtnElt //-2 for border:
- ? parseFloat(window.getComputedStyle(settingsBtnElt).height.slice(0,-2)) - 2
- : (smallScreen ? 31 : 37);
+ const settingsStyle = !!settingsBtnElt
+ ? window.getComputedStyle(settingsBtnElt)
+ : {width:"46px", height:"26px"};
+ const [indicWidth,indicHeight] = //[44,24];
+ [
+ // NOTE: -2 for border
+ parseFloat(settingsStyle.width.slice(0,-2)) - 2,
+ parseFloat(settingsStyle.height.slice(0,-2)) - 2
+ ];
+ let aboveBoardElts = [];
if (["chat","human"].includes(this.mode))
{
const connectedIndic = h(
'div',
{
"class": {
- "topindicator": true,
"indic-left": true,
"connected": this.oppConnected,
"disconnected": !this.oppConnected,
},
style: {
"width": indicWidth + "px",
- "height": indicWidth + "px",
+ "height": indicHeight + "px",
},
}
);
- elementArray.push(connectedIndic);
+ aboveBoardElts.push(connectedIndic);
}
if (this.mode == "chat")
{
},
'class': {
"tooltip": true,
- "topindicator": true,
+ "play": true,
+ "above-board": true,
"indic-left": true,
- "settings-btn": !smallScreen,
- "settings-btn-small": smallScreen,
},
},
[h('i', { 'class': { "material-icons": true } }, "chat")]
);
- elementArray.push(chatButton);
+ aboveBoardElts.push(chatButton);
}
else if (this.mode == "computer")
{
},
'class': {
"tooltip": true,
- "topindicator": true,
+ "play": true,
+ "above-board": true,
"indic-left": true,
- "settings-btn": !smallScreen,
- "settings-btn-small": smallScreen,
},
},
[h('i', { 'class': { "material-icons": true } }, "clear")]
);
- elementArray.push(clearButton);
+ aboveBoardElts.push(clearButton);
}
const turnIndic = h(
'div',
{
"class": {
- "topindicator": true,
"indic-right": true,
"white-turn": this.vr.turn=="w",
"black-turn": this.vr.turn=="b",
},
style: {
"width": indicWidth + "px",
- "height": indicWidth + "px",
+ "height": indicHeight + "px",
},
}
);
- elementArray.push(turnIndic);
+ aboveBoardElts.push(turnIndic);
const settingsBtn = h(
'button',
{
},
'class': {
"tooltip": true,
- "topindicator": true,
+ "play": true,
+ "above-board": true,
"indic-right": true,
- "settings-btn": !smallScreen,
- "settings-btn-small": smallScreen,
},
},
[h('i', { 'class': { "material-icons": true } }, "settings")]
);
- elementArray.push(settingsBtn);
+ aboveBoardElts.push(settingsBtn);
+ elementArray.push(
+ h('div',
+ { "class": { "aboveboard-wrapper": true } },
+ aboveBoardElts
+ )
+ );
if (this.mode == "problem")
{
// Show problem instructions
(!["idle","chat"].includes(this.mode) || this.cursor==this.vr.moves.length);
const gameDiv = h('div',
{
- 'class': { 'game': true },
+ 'class': {
+ 'game': true,
+ 'clearer': true,
+ },
},
[_.range(sizeX).map(i => {
let ci = (this.mycolor=='w' ? i : sizeX-i-1);
attrs: { "aria-label": 'Resign' },
'class': {
"tooltip":true,
+ "play": true,
"bottom": true,
- "small": smallScreen,
},
},
[h('i', { 'class': { "material-icons": true } }, "flag")])
on: { click: e => this.undo() },
attrs: { "aria-label": 'Undo' },
"class": {
- "small": smallScreen,
+ "play": true,
"spaceleft": true,
},
},
{
on: { click: e => this.play() },
attrs: { "aria-label": 'Play' },
- "class": { "small": smallScreen },
+ "class": {
+ "play": true,
+ "spaceleft": true,
+ },
},
[h('i', { 'class': { "material-icons": true } }, "fast_forward")]),
]
on: { click: this.undoInGame },
attrs: { "aria-label": 'Undo' },
"class": {
- "small": smallScreen,
+ "play": true,
"spaceleft": true,
},
},
{
on: { click: () => { this.mycolor = this.vr.getOppCol(this.mycolor) } },
attrs: { "aria-label": 'Flip' },
- "class": { "small": smallScreen },
+ "class": {
+ "play": true,
+ "spaceleft": true,
+ },
},
[h('i', { 'class': { "material-icons": true } }, "cached")]
),
{
myReservePiecesArray.push(h('div',
{
- 'class': {'board':true, ['board'+sizeY]:true},
+ 'class': {'board':true, ['board'+sizeY+'-reserve']:true},
attrs: { id: this.getSquareId({x:sizeX+shiftIdx,y:i}) }
},
[
h('img',
{
- 'class': {"piece":true},
+ 'class': {"piece":true, "reserve":true},
attrs: {
"src": "/images/pieces/" +
this.vr.getReservePpath(this.mycolor,i) + ".svg",
{
oppReservePiecesArray.push(h('div',
{
- 'class': {'board':true, ['board'+sizeY]:true},
+ 'class': {'board':true, ['board'+sizeY+'-reserve']:true},
attrs: { id: this.getSquareId({x:sizeX+(1-shiftIdx),y:i}) }
},
[
h('img',
{
- 'class': {"piece":true},
+ 'class': {"piece":true, "reserve":true},
attrs: {
"src": "/images/pieces/" +
this.vr.getReservePpath(oppCol,i) + ".svg",
),
h('button',
{
+ attrs: { id: "sendChatBtn"},
on: { click: this.sendChat },
domProps: { innerHTML: "Send" },
}
[
h('h3',
{
+ "class": { clickable: true },
domProps: { innerHTML: "Show solution" },
on: { click: this.toggleShowSolution },
}
{
'class': {
"col-sm-12":true,
- "col-md-8":true,
- "col-md-offset-2":true,
- "col-lg-6":true,
- "col-lg-offset-3":true,
+ "col-md-10":true,
+ "col-md-offset-1":true,
+ "col-lg-8":true,
+ "col-lg-offset-2":true,
},
// NOTE: click = mousedown + mouseup
on: {
Vue.component('my-problem-summary', {
props: ['prob','preview'],
template: `
- <div class="problem row" @click="showProblem()">
- <div class="col-sm-12 col-md-6 col-lg-3 diagram"
+ <div class="row problem clickable" @click="showProblem()">
+ <div class="col-sm-6 diagram"
v-html="getDiagram(prob.fen)">
</div>
- <div class="col-sm-12 col-md-6 col-lg-9">
+ <div class="col-sm-6">
<p v-html="prob.instructions"></p>
<p v-if="preview" v-html="prob.solution"></p>
<p v-else class="problem-time">{{ timestamp2date(prob.added) }}</p>
};
},
template: `
- <div>
- <button @click="fetchProblems('backward')">Previous</button>
- <button @click="fetchProblems('forward')">Next</button>
- <button @click="showNewproblemModal">New</button>
+ <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
+ <div id="problemControls" class="button-group">
+ <button aria-label="Load previous problems" class="tooltip"
+ @click="fetchProblems('backward')">
+ <i class="material-icons">skip_previous</i>
+ </button>
+ <button aria-label="Add a problem" class="tooltip"
+ @click="showNewproblemModal">
+ New
+ </button>
+ <button aria-label="Load next problems" class="tooltip"
+ @click="fetchProblems('forward')">
+ <i class="material-icons">skip_next</i>
+ </button>
+ </div>
<my-problem-summary v-on:show-problem="bubbleUp(p)"
v-for="(p,idx) in sortedProblems"
v-bind:prob="p" v-bind:preview="false" v-bind:key="idx">
<label for="modal-newproblem" class="modal-close"></label>
<my-problem-summary v-bind:prob="newProblem" v-bind:preview="true">
</my-problem-summary>
- <div class="col-sm-12 col-md-6 col-lg-3 col-lg-offset-3 topspace">
- <button @click="sendNewProblem()">Send</button>
+ <div class="button-group">
<button @click="newProblem.stage='nothing'">Cancel</button>
+ <button @click="sendNewProblem()">Send</button>
</div>
</div>
</div>
// Newest problem first
return this.problems.sort((p1,p2) => { return p2.added - p1.added; });
},
- mailErrProblem: function() {
- return "mailto:contact@vchess.club?subject=[" + variant + " problems] error";
- },
},
methods: {
// Propagate "show problem" event to parent component (my-variant)
this.$emit('show-problem', JSON.stringify(problem));
},
fetchProblems: function(direction) {
- return; //TODO: re-activate after server side is implemented (see routes/all.js)
if (this.problems.length == 0)
return; //what could we do?!
// Search for newest date (or oldest)
previewNewProblem: function() {
if (!V.IsGoodFen(this.newProblem.fen))
return alert("Bad FEN string");
+ if (this.newProblem.instructions.length == 0)
+ return alert("Empty instructions");
+ if (this.newProblem.solution.length == 0)
+ return alert("Empty solution");
this.newProblem.stage = "preview";
},
sendNewProblem: function() {
data: function() {
return { content: "" };
},
- template: `<div v-html="content" class="section-content"></div>`,
+ template: `
+ <div class="col-sm-12 col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
+ <div v-html="content" class="section-content"></div>
+ </div>
+ `,
mounted: function() {
// AJAX request to get rules content (plain text, HTML)
ajax("/rules/" + variant, "GET", response => {
new Vue({
el: "#variantPage",
data: {
- display: "game", //default: play!
+ display: "play", //default: play!
problem: undefined, //current problem in view
},
created: function() {
const url = window.location.href;
const hashPos = url.indexOf("#");
+ console.log(hashPos + " " + url);
if (hashPos >= 0)
this.setDisplay(url.substr(hashPos+1));
},
methods: {
showProblem: function(problemTxt) {
this.problem = JSON.parse(problemTxt);
- this.display = "game";
+ this.display = "play";
},
setDisplay: function(elt) {
this.display = elt;
- document.getElementById("drawer-control").checked = false;
+ let menuToggle = document.getElementById("drawer-control");
+ if (!!menuToggle)
+ menuToggle.checked = false;
},
},
});
// 6) Check promoted array
if (!fenParsed.promoted)
return false;
- fenpromoted = fenParsed.promoted;
- if (fenpromoted == "-")
+ if (fenParsed.promoted == "-")
return true; //no promoted piece on board
- const squares = fenpromoted.split(",");
+ const squares = fenParsed.promoted.split(",");
for (let square of squares)
{
const c = V.SquareToCoords(square);
getReserveFen()
{
- let counts = _.map(_.range(10), 0);
- for (let i=0; i<V.PIECES.length; i++)
+ let counts = new Array(10);
+ for (let i=0; i<V.PIECES.length-1; i++) //-1: no king reserve
{
counts[i] = this.reserve["w"][V.PIECES[i]];
counts[5+i] = this.reserve["b"][V.PIECES[i]];
#helpMenu
float: right
- cursor: pointer
@media screen and (max-width: 767px)
.info-container
p
#flagMenu
float: right
- cursor: pointer
margin-right: 10px
@media screen and (max-width: 767px)
margin-right: 5px
margin-top: 0
color: var(--a-link-color)
text-decoration: underline
- cursor: pointer
#welcome
max-width: 767px
text-align: left
border: 0
#disableMsg
- cursor: pointer
color: darkred
.emphasis
font-style: italic
+.clickable
+ cursor: pointer
+
+.clearer
+ clear: both
+
.red
color: #cc3300
#menuBar
background: linear-gradient(#e66465, #9198e5)
- height: 77px
+ height: 29px
margin-bottom: 10px
@media screen and (max-width: 767px)
height: 100%
margin-bottom: 0
@media screen and (min-width: 768px)
width: 100%
+ overflow: hidden
a#homeLink
- margin: 27px 0 0 10px
+ margin-left: 10px
+ margin-top: 2px
+ color: black
display: inline-block
@media screen and (max-width: 767px)
- margin: 10px 0 0 10px
display: block
+ margin: 5px 0 0 12px
.info-container
display: inline-block
color: black
a, p
display: inline-block
- padding: 3px
+ padding: 0
border: 1px solid black;
- margin: 25px 0 0 15px
+ margin: 1px 0 0 15px
@media screen and (max-width: 767px)
margin-top: 10px
display: block
#helpMenu
@media screen and (min-width: 768px)
float: right
- cursor: pointer
- @media screen and (max-width: 767px)
- .info-container
- p
- margin-right: 5px
+ .info-container
+ p
+ margin: 1px 0 0 15px
#flagMenu
@media screen and (min-width: 768px)
+ margin-top: 1px
float: right
- cursor: pointer
margin: 0 15px
@media screen and (max-width: 767px)
- margin-right: 5px
+ margin: 25px 5px 0 15px
img
display: inline-block
- height: 30px
- margin-top: 25px
+ margin: 0
+ height: 25px
label.drawer-toggle
padding: 0
&::before
- font-size: 2.5em;
- max-height: 43px;
- top: -10px;
- left: 10px
+ font-size: 2em;
+ max-height: 32px;
+ top: -7px;
+ left: 5px
// Game section:
-.topindicator
- position: relative
+button.play
+ height: 24px
+ margin: 0
+ padding: 0 10px 24px 10px
+ box-sizing: border-box
border: 1px solid brown
+button.play.spaceleft
+ margin-left: 15px
+button.play.spaceright
+ margin-right: 15px
+
+.aboveboard-wrapper
+ width: 80vh
+ margin: 0 auto
+ @media screen and (max-width: 767px)
+ width: 100%
+ margin: 0
+
+button.above-board
+ margin-left: 15px
+ margin-right: 15px
+
+i.material-icons
+ font-size: 24px
.indic-left
+ border: 1px solid brown
float: left
- margin: 0 0 var(--universal-margin) 20px
+ margin: 0 0 var(--universal-margin) 10vh
+ @media screen and (max-width: 767px)
+ margin-left: 20px
.indic-right
+ border: 1px solid brown
float: right
- margin: 0 20px var(--universal-margin) 0
+ margin: 0 10vh var(--universal-margin) 0
+ @media screen and (max-width: 767px)
+ margin-right: 20px
.my-chatmsg
color: black
.opp-chatmsg
color: blue
+// TODO: this fix is not good (button height 0 if chat overflow window height)
+#sendChatBtn
+ min-height: 42px
+
.connected
background-color: green
.disconnected
background-color: red
-.settings-btn
- padding: 6px 7px 0 7px
-
-.settings-btn-small
- padding: 0 3px
-
.white-turn
background-color: white
&:hover
background-color: #cc99ff
+.game.reserve-div
+ margin-bottom: 18px
+
.reserve-count
padding-left: 40%
-.reserve-div
- margin-bottom: 20px
-
.reserve-row-1
margin-bottom: 15px
width: 12.5%
padding-bottom: 12.5%
+div.board8-reserve
+ width: 10%
+ padding-bottom: 10%
+
div.board10
width: 10%
padding-bottom: 10%
width: 9.09%
padding-bottom: 9.1%
+// NOTE: no variants with reserve of size != 8
+
.game
- clear: both
+ width: 80vh
+ margin: 0 auto
.board
cursor: pointer
+ @media screen and (max-width: 767px)
+ width: 100%
+ margin: 0
#choices
margin: 0 auto 0 auto
margin-left: 0
margin-right: 0
+#modal-eog+div .card
+ overflow: hidden
+
+#actions
+ margin: 10px 0
+
// Rules section:
.warn
clear: both
padding-top: 5px
-.spaceleft
- margin-left: 30px
-
p.boxed
background-color: #FFCC66
padding: 5px
#problem-solution
display: none
-.topspace
- margin-top: 15px
-
-.problem
- cursor: pointer
- margin-bottom: 15px
-
#solution-div h3
- cursor: pointer
+ background-color: lightgrey
+ padding: 3px 5px
.newproblem-form, .newproblem-preview
max-width: 90%
-.clickable
- cursor: pointer
+#problemControls
+ width: 75%
+ margin: 0 auto
+ @media screen and (max-width: 767px)
+ width: 100%
+ margin: 0
-.clearer
- clear: both
+.problem
+ margin: 10px 0
const sqlite3 = require('sqlite3');//.verbose();
const db = new sqlite3.Database('db/vchess.sqlite');
const sanitizeHtml = require('sanitize-html');
+const MaxNbProblems = 2;
const supportedLang = ["fr","en"];
function selectLanguage(req, res)
});
// Variant
-router.get("/:vname([a-zA-Z0-9]+)", (req,res,next) => {
- const vname = req.params["vname"];
+router.get("/:variant([a-zA-Z0-9]+)", (req,res,next) => {
+ const vname = req.params["variant"];
db.serialize(function() {
db.all("SELECT * FROM Variants WHERE name='" + vname + "'", (err,variant) => {
if (!!err)
return next(err);
if (!variant || variant.length==0)
return next(createError(404));
- // TODO (later...) get only n=100(?) most recent problems
- db.all("SELECT * FROM Problems WHERE variant='" + vname + "'",
- (err2,problems) => {
- if (!!err2)
- return next(err2);
- res.render('variant', {
- title: vname + ' Variant',
- variant: vname,
- problemArray: problems,
- lang: selectLanguage(req, res),
- languages: supportedLang,
- });
- }
- );
+ // Get only N most recent problems
+ const query2 = "SELECT * FROM Problems " +
+ "WHERE variant='" + vname + "' " +
+ "ORDER BY added DESC " +
+ "LIMIT " + MaxNbProblems;
+ db.all(query2, (err2,problems) => {
+ if (!!err2)
+ return next(err2);
+ res.render('variant', {
+ title: vname + ' Variant',
+ variant: vname,
+ problemArray: problems,
+ lang: selectLanguage(req, res),
+ languages: supportedLang,
+ });
+ });
});
});
});
res.render("rules/" + req.params["variant"] + "/" + lang);
});
-// Fetch 10 previous or next problems (AJAX)
+// Fetch N previous or next problems (AJAX)
router.get("/problems/:variant([a-zA-Z0-9]+)", (req,res) => {
if (!req.xhr)
return res.json({errmsg: "Unauthorized access"});
- // TODO: next or previous: in params + timedate (of current oldest or newest)
+ const vname = req.params["variant"];
+ const directionStr = (req.query.direction == "forward" ? ">" : "<");
+ const lastDt = req.query.last_dt;
+ if (!lastDt.match(/[0-9]+/))
+ return res.json({errmsg: "Bad timestamp"});
db.serialize(function() {
- //TODO
+ const query = "SELECT * FROM Problems " +
+ "WHERE variant='" + vname + "' " +
+ " AND added " + directionStr + " " + lastDt + " " +
+ "ORDER BY added " + (directionStr=="<" ? "DESC " : "") +
+ "LIMIT " + MaxNbProblems;
+ db.all(query, (err,problems) => {
+ if (!!err)
+ return res.json(err);
+ return res.json({problems: problems});
+ });
});
});
const fen = req.body["fen"];
if (!fen.match(/^[a-zA-Z0-9, /-]*$/))
return res.json({errmsg: "Bad characters in FEN string"});
- const instructions = sanitizeHtml(req.body["instructions"]);
- const solution = sanitizeHtml(req.body["solution"]);
+ const instructions = sanitizeHtml(req.body["instructions"]).trim();
+ const solution = sanitizeHtml(req.body["solution"]).trim();
+ if (instructions.length == 0)
+ return res.json({errmsg: "Empty instructions"});
+ if (solution.length == 0)
+ return res.json({errmsg: "Empty solution"});
db.serialize(function() {
let stmt = db.prepare("INSERT INTO Problems " +
"(added,variant,fen,instructions,solution) VALUES (?,?,?,?,?)");
.info-container
p vchess.club
img(src="/images/index/wildebeest.svg")
- #flagMenu(onClick="document.getElementById('modalLang').checked=true")
+ #flagMenu.clickable(
+ onClick="document.getElementById('modalLang').checked=true")
img(src="/images/flags/" + lang + ".svg")
- #helpMenu(onClick="document.getElementById('modalHelp').checked=true")
+ #helpMenu.clickable(
+ onClick="document.getElementById('modalHelp').checked=true")
.info-container
p Help
.row
div(role="dialog")
#b4welcome.card.text-center.small-modal
h3.blue First visit?
- p#readThis(@click="showWelcomeMsg") >>> Please read this <<<
+ p#readThis.clickable(@click="showWelcomeMsg") >>> Please read this <<<
case lang
when "en"
include welcome/en.pug
input#drawer-control.drawer(type="checkbox")
#menuBar
label.drawer-close(for="drawer-control")
- a#homeLink.conditional-jump(href="/")
+ a#homeLink(href="/")
i.material-icons home
.info-container
- a.conditional-jump(href="#rules" @click="setDisplay('rules')")
+ a(href="#rules" @click="setDisplay('rules')")
| Rules
- a.conditional-jump(href="#play" @click="setDisplay('game')")
+ a(href="#play" @click="setDisplay('play')")
| Play!
- a.conditional-jump(href="#problems" @click="setDisplay('problems')")
+ a(href="#problems" @click="setDisplay('problems')")
| Problems
- #flagMenu.conditional-jump(
+ #flagMenu.clickable(
onClick="document.getElementById('modalLang').checked=true")
img(src="/images/flags/" + lang + ".svg")
- #helpMenu.conditional-jump(
+ #helpMenu.clickable(
onClick="document.getElementById('modalHelp').checked=true")
.info-container
p Help
.row
- .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2(
- v-show="display=='rules'")
- my-rules
- .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2(
- v-show="display=='game'")
- my-game(v-bind:problem="problem")
- .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2(
- v-show="display=='problems'")
- my-problems(v-on:show-problem="showProblem($event)")
+ my-rules(v-show="display=='rules'")
+ my-game(v-show="display=='play'" v-bind:problem="problem")
+ my-problems(v-show="display=='problems'" v-on:show-problem="showProblem($event)")
// (Some) Modals:
include modal-help.pug
include modal-lang.pug
For informations about hundreds (if not thousands) of variants, you
can visit the excellent
#[a(href="https://www.chessvariants.com/") chessvariants] website.
- p#disableMsg(@click="markAsVisited")
+ p#disableMsg.clickable(@click="markAsVisited")
| Click here to not show this message next time
p.smallfont Image credit: #[a(href=wikipediaUrl) Wikipedia]
Pour s'informer sur des centaines de variantes (au moins), je vous invite à
visiter l'excellent site
#[a(href="https://www.chessvariants.com/") chessvariants].
- p#disableMsg(@click="markAsVisited")
+ p#disableMsg.clickable(@click="markAsVisited")
| Cliquer ici pour ne pas montrer ce message la prochaine fois
p.smallfont Crédit image: #[a(href=wikipediaUrl) Wikipedia]