+#Enhancements
+
+tabs "running" and "completed" for MyGames page (default to running if any and my turn)
+"load more" option for completed games (act on corr games).
+
+"load more" option for problems as well: similar to news page.
+Also on corr challenges.
+
# New variants
Landing pieces from empty board:
https://www.chessvariants.com/diffsetup.dir/unachess.html
.col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
footer
router-link.menuitem(to="/about") {{ st.tr["About"] }}
+ router-link.menuitem#newsMenu(to="/news") {{ st.tr["News"] }}
+ a.menuitem(href="https://discord.gg/a9ZFKBe")
+ span Discord
+ img(src="/images/icons/discord.svg")
a.menuitem(href="https://github.com/yagu0/vchess")
span {{ st.tr["Code"] }}
img(src="/images/icons/github.svg")
- router-link.menuitem(to="/news") {{ st.tr["News"] }}
p.clickable(onClick="window.doClick('modalContact')")
| {{ st.tr["Contact"] }}
</template>
import Settings from "@/components/Settings.vue";
import UpsertUser from "@/components/UpsertUser.vue";
import { store } from "@/store.js";
+import { ajax } from "@/utils/ajax.js";
export default {
components: {
ContactForm,
UpsertUser
},
data: function() {
- return {
- st: store.state
- };
+ return { st: store.state };
+ },
+ mounted: function() {
+ ajax(
+ "/newsts",
+ "GET",
+ {
+ success: (res) => {
+ if (this.st.user.newsRead < res.timestamp)
+ document.getElementById("newsMenu").classList.add("somenews");
+ }
+ }
+ );
},
methods: {
hideDrawer: function(e) {
footer
border: none
+@media screen and (max-width: 420px)
+ footer
+ display: block
+
+.menuitem.somenews
+ color: red
+ &:link, &:visited, &:hover
+ color: red
+
// Styles for diagrams and board (partial).
// TODO: where to put that ?
)
.card
label.modal-close(for="modalContact")
- a#discordLink(href="https://discord.gg/a9ZFKBe")
- span {{ st.tr["Discord invitation"] }}
- img(src="/images/icons/discord.svg")
fieldset
label(for="userEmail") {{ st.tr["Email"] }}
input#userEmail(type="email" :value="st.user.email")
width: 100%
min-height: 100px
-#discordLink
- display: block
- margin-top: 7px
- text-align: center
- & > img
- height: 1.2em
- display: inline-block
- margin-left: 5px
-
#dialog
padding: 5px
color: blue
name: localStorage.getItem("myname") || "", //"" for "anonymous"
email: "", //unknown yet
notify: false, //email notifications
+ newsRead: localStorage.getItem("newsRead") || 0,
sid: mysid
};
// Slow verification through the server:
localStorage.removeItem("myname");
this.state.user.email = json.email;
this.state.user.notify = json.notify;
+ if (!!json.newsRead && json.newsRead > this.state.user.newsRead)
+ this.state.user.newsRead = json.newsRead;
});
// Settings initialized with values from localStorage
const getItemDefaultTrue = (item) => {
// TODO: use this syntax https://stackoverflow.com/a/29823632 ?
// data, success, error: optional
export function ajax(url, method, options) {
+ options = options || {};
const data = options.data || {};
// By default, do nothing on success and print errors:
if (!options.success)
getPotentialMovesFrom([x, y]) {
// At subTurn == 2, jailers aren't effective (Jeff K)
+ const piece = this.getPiece(x, y);
+ const L = this.sentryPush.length;
if (this.subTurn == 1) {
const jsq = this.isImmobilized([x, y]);
if (!!jsq) {
let moves = [];
// Special pass move if king:
- if (this.getPiece(x, y) == V.KING) {
+ if (piece == V.KING) {
moves.push(
new Move({
appear: [],
})
);
}
+ else if (piece == V.LANCER && !!this.sentryPush[L-1]) {
+ // A pushed lancer next to the jailer: reorient
+ const color = this.getColor(x, y);
+ const curDir = this.board[x][y].charAt(1);
+ Object.keys(V.LANCER_DIRS).forEach(k => {
+ moves.push(
+ new Move({
+ appear: [{ x: x, y: y, c: color, p: k }],
+ vanish: [{ x: x, y: y, c: color, p: curDir }],
+ start: { x: x, y: y },
+ end: { x: jsq[0], y: jsq[1] }
+ })
+ );
+ });
+ }
return moves;
}
}
let moves = [];
- switch (this.getPiece(x, y)) {
+ switch (piece) {
case V.JAILER:
moves = this.getPotentialJailerMoves([x, y]);
break;
moves = super.getPotentialMovesFrom([x, y]);
break;
}
- const L = this.sentryPush.length;
if (!!this.sentryPush[L-1]) {
- // Delete moves walking back on sentry push path
+ // Delete moves walking back on sentry push path,
+ // only if not a pawn, and the piece is the pushed one.
+ const pl = this.sentryPush[L-1].length;
+ const finalPushedSq = this.sentryPush[L-1][pl-1];
moves = moves.filter(m => {
if (
m.vanish[0].p != V.PAWN &&
+ m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y &&
this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
) {
return false;
);
},
mounted: function() {
+ // Mark that I've read the news:
+ localStorage.setItem("newsRead", Date.now());
+ if (this.st.user.id > 0) ajax("/newsread", "PUT");
+ document.getElementById("newsMenu").classList.remove("somenews");
document
.getElementById("newnewsDiv")
.addEventListener("click", processModalClick);
loginTime datetime,
sessionToken varchar,
created datetime,
- notify boolean
+ notify boolean,
+ newsRead datetime
);
create table Problems (
});
},
+ getTimestamp: function(cb)
+ {
+ db.serialize(function() {
+ const query =
+ "SELECT added " +
+ "FROM News " +
+ "ORDER BY added DESC " +
+ "LIMIT 1";
+ db.get(query, (err,ts) => {
+ cb(err, ts);
+ });
+ });
+ },
+
update: function(news)
{
db.serialize(function() {
});
},
+ setNewsRead: function(uid)
+ {
+ db.serialize(function() {
+ const query =
+ "UPDATE Users " +
+ "SET newsRead = " + Date.now() + " " +
+ "WHERE id = " + uid;
+ db.run(query);
+ });
+ },
+
// Set session token only if empty (first login)
// NOTE: weaker security (but avoid to re-login everywhere after each logout)
// TODO: option would be to reset all tokens periodically, e.g. every 3 months
{
const content = sanitizeHtml(req.body.news.content);
NewsModel.create(content, req.userId, (err,ret) => {
- res.json(err || {id:ret.nid});
+ res.json(err || { id: ret.nid });
});
}
});
router.get("/news", access.ajax, (req,res) => {
const cursor = req.query["cursor"];
- if (cursor.match(/^[0-9]+$/))
- {
+ if (cursor.match(/^[0-9]+$/)) {
NewsModel.getNext(cursor, (err,newsList) => {
- res.json(err || {newsList:newsList});
+ res.json(err || { newsList: newsList });
});
}
});
+router.get("/newsts", access.ajax, (req,res) => {
+ // Special query for footer: just return timestamp of last news
+ NewsModel.getTimestamp((err,ts) => {
+ res.json(err || { timestamp: ts.added });
+ });
+});
+
router.put("/news", access.logged, access.ajax, (req,res) => {
let news = req.body.news;
- if (devs.includes(req.userId) && news.id.toString().match(/^[0-9]+$/))
- {
+ if (devs.includes(req.userId) && news.id.toString().match(/^[0-9]+$/)) {
news.content = sanitizeHtml(news.content);
NewsModel.update(news);
res.json({});
router.delete("/news", access.logged, access.ajax, (req,res) => {
const nid = req.query.id;
- if (devs.includes(req.userId) && nid.toString().match(/^[0-9]+$/))
- {
+ if (devs.includes(req.userId) && nid.toString().match(/^[0-9]+$/)) {
NewsModel.remove(nid);
res.json({});
}
}
});
+// Special route to update newsRead timestamp:
+router.put('/newsread', access.logged, access.ajax, (req,res) => {
+ UserModel.setNewsRead(req.userId);
+ res.json({});
+});
+
// Authentication-related methods:
// to: object user (to who we send an email)