diff --git a/app/views/coordinate/home.scala.html b/app/views/coordinate/home.scala.html index 7f0842b6b3..86eb034ad6 100644 --- a/app/views/coordinate/home.scala.html +++ b/app/views/coordinate/home.scala.html @@ -30,7 +30,7 @@ active = siteMenu.puzzle.some) {

@trans.training()

Puzzle - Coordinate + Coord
@if(ctx.isAuth) { @scoreOption.map { score => diff --git a/app/views/opening/JsData.scala b/app/views/opening/JsData.scala index 0e373ad77d..46efe01f82 100644 --- a/app/views/opening/JsData.scala +++ b/app/views/opening/JsData.scala @@ -19,13 +19,19 @@ object JsData extends lila.Steroids { "attempts" -> opening.attempts, "fen" -> opening.fen, "color" -> opening.color.name, - "moves" -> JsArray(opening.scoredMoves.map { - case ScoredMove(move, score) => Json.obj( + "moves" -> JsArray(opening.qualityMoves.map { + case QualityMove(move, quality) => Json.obj( "first" -> move.first, "cp" -> move.cp, "line" -> move.line.mkString(" "), - "score" -> score.name) + "quality" -> quality.name) }), "url" -> s"$netBaseUrl${routes.Opening.show(opening.id)}" - )))) + ), + "user" -> userInfos.map { i => + Json.obj( + "score" -> i.score, + "history" -> i.history.nonEmpty.option(Json.toJson(i.chart)) + ) + }))) } diff --git a/app/views/opening/show.scala.html b/app/views/opening/show.scala.html index 9ced7ca44a..3b15552d98 100644 --- a/app/views/opening/show.scala.html +++ b/app/views/opening/show.scala.html @@ -9,7 +9,10 @@ document.querySelector('#lichess .round'), @JsData(opening, userInfos), openingRoutes.controllers, @Html(J.stringify(i18nJsObject( -trans.training +trans.training, +trans.toTrackYourProgress, +trans.signUp, +trans.trainingSignupExplanation ))) ); } diff --git a/modules/opening/src/main/Opening.scala b/modules/opening/src/main/Opening.scala index a1fe63ee10..52bab34740 100644 --- a/modules/opening/src/main/Opening.scala +++ b/modules/opening/src/main/Opening.scala @@ -17,29 +17,40 @@ case class Opening( attempts: Int, score: Double) { - def scoredMoves = moves.map { move => - ScoredMove(move, Score.Good) + def qualityMoves: List[QualityMove] = { + val bestCp = moves.foldLeft(Int.MaxValue) { + case (cp, move) => if (move.cp < cp) move.cp else cp + } + moves.map { move => + QualityMove(move, Quality(move.cp - bestCp)) + } } } -sealed trait Score { - def name = toString.toLowerCase +sealed abstract class Quality(val threshold: Int) { + val name = toString.toLowerCase } -object Score { - case object Great extends Score - case object Good extends Score - case object Dubious extends Score - case object Bad extends Score +object Quality { + case object Good extends Quality(30) + case object Dubious extends Quality(80) + case object Bad extends Quality(Int.MaxValue) + + def apply(cp: Int) = + if (cp < Good.threshold) Good + else if (cp < Dubious.threshold) Dubious + else Bad } -case class ScoredMove( +case class QualityMove( move: Move, - score: Score) + quality: Quality) object Opening { type ID = Int + val defaultScore = 50 + def make( fen: String, color: Color, diff --git a/modules/opening/src/main/UserInfos.scala b/modules/opening/src/main/UserInfos.scala index f9960233f4..16aef0c0f4 100644 --- a/modules/opening/src/main/UserInfos.scala +++ b/modules/opening/src/main/UserInfos.scala @@ -2,27 +2,33 @@ package lila.opening import reactivemongo.bson._ import reactivemongo.bson.Macros +import play.api.libs.json._ import lila.db.Types.Coll import lila.rating.Glicko import lila.user.User -case class UserInfos(user: User, history: List[Attempt]) +case class UserInfos(user: User, history: List[Attempt], chart: JsArray) { + + def score = if (history.isEmpty) 50f + else history.foldLeft(0)(_ + _.score) / history.size +} object UserInfos { private def historySize = 20 + private def chartSize = 12 import Attempt.attemptBSONHandler def apply(attemptColl: Coll) = new { def apply(user: User): Fu[UserInfos] = fetchAttempts(user.id) map { attempts => - new UserInfos(user, makeHistory(attempts)) + new UserInfos(user, makeHistory(attempts), makeChart(attempts)) } recover { case e: Exception => - play.api.Logger("Puzzle UserInfos").error(e.getMessage) - new UserInfos(user, Nil) + play.api.Logger("Opening UserInfos").error(e.getMessage) + new UserInfos(user, Nil, JsArray()) } def apply(user: Option[User]): Fu[Option[UserInfos]] = @@ -33,8 +39,14 @@ object UserInfos { Attempt.BSONFields.userId -> userId )).sort(BSONDocument( Attempt.BSONFields.date -> -1 - )).cursor[Attempt].collect[List](historySize) + )).cursor[Attempt].collect[List](math.max(historySize, chartSize)) } private def makeHistory(attempts: List[Attempt]) = attempts.take(historySize) + + private def makeChart(attempts: List[Attempt]) = JsArray { + val scores = attempts.take(chartSize).reverse map (_.score) + val filled = List.fill(chartSize - scores.size)(Glicko.default.intRating) ::: scores + filled map { JsNumber(_) } + } } diff --git a/public/stylesheets/opening.css b/public/stylesheets/opening.css index e69de29bb2..b9427e0155 100644 --- a/public/stylesheets/opening.css +++ b/public/stylesheets/opening.css @@ -0,0 +1,211 @@ +#opening .lulzbar { + position: relative; + display: block; + width: 472px; + height: 20px; + padding: 10px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 16px; + margin: 20px 0 0 0; + box-shadow: 0px 4px 4px -4px rgba(255, 255, 255, 0.4), 0px -3px 3px -3px rgba(255, 255, 255, 0.25), inset 0px 0px 12px 0px rgba(0, 0, 0, 0.5); +} +#opening .lulzbar:before { + position: absolute; + display: block; + content: ""; + width: 470px; + height: 18px; + top: 10px; + left: 20px; + border-radius: 20px; + background: #222; + box-shadow: inset 0px 0px 6px 0px rgba(0, 0, 0, 0.85); + border: 1px solid rgba(0, 0, 0, 0.8); +} +#opening .lulzbar .bar { + position: absolute; + display: block; + width: 0px; + height: 16px; + top: 12px; + left: 22px; + background: rgb(126, 234, 25); + background: -moz-linear-gradient(top, rgba(126, 234, 25, 1) 0%, rgba(83, 173, 0, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(126, 234, 25, 1)), color-stop(100%, rgba(83, 173, 0, 1))); + background: -webkit-linear-gradient(top, rgba(126, 234, 25, 1) 0%, rgba(83, 173, 0, 1) 100%); + background: -o-linear-gradient(top, rgba(126, 234, 25, 1) 0%, rgba(83, 173, 0, 1) 100%); + background: -ms-linear-gradient(top, rgba(126, 234, 25, 1) 0%, rgba(83, 173, 0, 1) 100%); + background: linear-gradient(to bottom, rgba(126, 234, 25, 1) 0%, rgba(83, 173, 0, 1) 100%); + border-radius: 16px; + box-shadow: 0px 0px 12px 0px rgba(126, 234, 25, 1), inset 0px 1px 0px 0px rgba(255, 255, 255, 0.45), inset 1px 0px 0px 0px rgba(255, 255, 255, 0.25), inset -1px 0px 0px 0px rgba(255, 255, 255, 0.25); + overflow: hidden; + transition: width 1s; +} +#opening .lulzbar .bar.yellow { + background: rgb(229, 195, 25); + background: -moz-linear-gradient(top, rgba(229, 195, 25, 1) 0%, rgba(168, 140, 0, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(229, 195, 25, 1)), color-stop(100%, rgba(168, 140, 0, 1))); + background: -webkit-linear-gradient(top, rgba(229, 195, 25, 1) 0%, rgba(168, 140, 0, 1) 100%); + background: -o-linear-gradient(top, rgba(229, 195, 25, 1) 0%, rgba(168, 140, 0, 1) 100%); + background: -ms-linear-gradient(top, rgba(229, 195, 25, 1) 0%, rgba(168, 140, 0, 1) 100%); + background: linear-gradient(to bottom, rgba(229, 195, 25, 1) 0%, rgba(168, 140, 0, 1) 100%); + box-shadow: 0px 0px 12px 0px rgba(229, 195, 25, 1), inset 0px 1px 0px 0px rgba(255, 255, 255, 0.45), inset 1px 0px 0px 0px rgba(255, 255, 255, 0.25), inset -1px 0px 0px 0px rgba(255, 255, 255, 0.25); +} +#opening .lulzbar .bar.red { + background: rgb(232, 25, 87); + background: -moz-linear-gradient(top, rgba(232, 25, 87, 1) 0%, rgba(170, 0, 51, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(232, 25, 87, 1)), color-stop(100%, rgba(170, 0, 51, 1))); + background: -webkit-linear-gradient(top, rgba(232, 25, 87, 1) 0%, rgba(170, 0, 51, 1) 100%); + background: -o-linear-gradient(top, rgba(232, 25, 87, 1) 0%, rgba(170, 0, 51, 1) 100%); + background: -ms-linear-gradient(top, rgba(232, 25, 87, 1) 0%, rgba(170, 0, 51, 1) 100%); + background: linear-gradient(to bottom, rgba(232, 25, 87, 1) 0%, rgba(170, 0, 51, 1) 100%); + box-shadow: 0px 0px 12px 0px rgba(232, 25, 87, 1), inset 0px 1px 0px 0px rgba(255, 255, 255, 0.45), inset 1px 0px 0px 0px rgba(255, 255, 255, 0.25), inset -1px 0px 0px 0px rgba(255, 255, 255, 0.25); +} +#opening .lulzbar .bar.blue { + background: rgb(24, 109, 226); + background: -moz-linear-gradient(top, rgba(24, 109, 226, 1) 0%, rgba(0, 69, 165, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(24, 109, 226, 1)), color-stop(100%, rgba(0, 69, 165, 1))); + background: -webkit-linear-gradient(top, rgba(24, 109, 226, 1) 0%, rgba(0, 69, 165, 1) 100%); + background: -o-linear-gradient(top, rgba(24, 109, 226, 1) 0%, rgba(0, 69, 165, 1) 100%); + background: -ms-linear-gradient(top, rgba(24, 109, 226, 1) 0%, rgba(0, 69, 165, 1) 100%); + background: linear-gradient(to bottom, rgba(24, 109, 226, 1) 0%, rgba(0, 69, 165, 1) 100%); + box-shadow: 0px 0px 12px 0px rgba(24, 109, 226, 1), inset 0px 1px 0px 0px rgba(255, 255, 255, 0.45), inset 1px 0px 0px 0px rgba(255, 255, 255, 0.25), inset -1px 0px 0px 0px rgba(255, 255, 255, 0.25); +} +#opening .lulzbar .bar:before { + position: absolute; + display: block; + content: ""; + width: 606px; + height: 150%; + top: -25%; + left: -25px; + background: -moz-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.01) 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, rgba(255, 255, 255, 0.35)), color-stop(50%, rgba(255, 255, 255, 0.01)), color-stop(51%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 0))); + background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.01) 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%); + background: -o-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.01) 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%); + background: -ms-radial-gradient(center, ellipse cover, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.01) 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%); + background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0.01) 50%, rgba(255, 255, 255, 0) 51%, rgba(255, 255, 255, 0) 100%); +} +#opening .lulzbar .bar:after { + position: absolute; + display: block; + content: ""; + width: 64px; + height: 16px; + right: 0; + top: 0; + border-radius: 0px 16px 16px 0px; + background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 98%, rgba(255, 255, 255, 0) 100%); + background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(98%, rgba(255, 255, 255, 0.6)), color-stop(100%, rgba(255, 255, 255, 0))); + background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 98%, rgba(255, 255, 255, 0) 100%); + background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 98%, rgba(255, 255, 255, 0) 100%); + background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 98%, rgba(255, 255, 255, 0) 100%); + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 98%, rgba(255, 255, 255, 0) 100%); +} +#opening .lulzbar .bar span { + position: absolute; + display: block; + width: 100%; + height: 64px; + border-radius: 16px; + top: 0; + left: 0; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAG4AAABACAYAAAD7/UK9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIFdpbmRvd3MiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MjdFQ0M2MzdDQThBMTFFMUE3NzJFNzY4M0ZDMTA3MTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MjdFQ0M2MzhDQThBMTFFMUE3NzJFNzY4M0ZDMTA3MTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoyN0VDQzYzNUNBOEExMUUxQTc3MkU3NjgzRkMxMDcxMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoyN0VDQzYzNkNBOEExMUUxQTc3MkU3NjgzRkMxMDcxMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoTG0pMAABr+SURBVHjavJ1nj1zX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") 0 0; + -webkit-animation: sparkle 1500ms linear infinite; + -moz-animation: sparkle 1500ms linear infinite; + -o-animation: sparkle 1500ms linear infinite; + animation: sparkle 1500ms linear infinite; + opacity: 0.2; +} +#opening .lulzbar .label { + font-family: monospace; + position: absolute; + display: block; + width: 40px; + height: 30px; + line-height: 30px; + top: 38px; + left: 0px; + background: rgb(76, 76, 76); + background: -moz-linear-gradient(top, rgba(76, 76, 76, 1) 0%, rgba(38, 38, 38, 1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(76, 76, 76, 1)), color-stop(100%, rgba(38, 38, 38, 1))); + background: -webkit-linear-gradient(top, rgba(76, 76, 76, 1) 0%, rgba(38, 38, 38, 1) 100%); + background: -o-linear-gradient(top, rgba(76, 76, 76, 1) 0%, rgba(38, 38, 38, 1) 100%); + background: -ms-linear-gradient(top, rgba(76, 76, 76, 1) 0%, rgba(38, 38, 38, 1) 100%); + background: linear-gradient(to bottom, rgba(76, 76, 76, 1) 0%, rgba(38, 38, 38, 1) 100%); + font-weight: bold; + font-size: 14px; + color: #fff; + text-align: center; + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.3); + text-shadow: 0px -1px 0px #000000, 0px 1px 1px #000000; + filter: dropshadow(color=#000000, offx=0, offy=-1); + transition: left 1s; +} +#opening .lulzbar .label span { + position: absolute; + display: block; + width: 12px; + height: 9px; + top: -9px; + left: 14px; + background: transparent; + overflow: hidden; +} +#opening .lulzbar .label span:before { + position: absolute; + display: block; + content: ""; + width: 8px; + height: 8px; + top: 4px; + left: 2px; + border: 1px solid rgba(0, 0, 0, 0.5); + background: rgb(86, 86, 86); + background: -moz-linear-gradient(-45deg, rgba(86, 86, 86, 1) 0%, rgba(76, 76, 76, 1) 50%); + background: -webkit-gradient(linear, left top, right bottom, color-stop(0%, rgba(86, 86, 86, 1)), color-stop(50%, rgba(76, 76, 76, 1))); + background: -webkit-linear-gradient(-45deg, rgba(86, 86, 86, 1) 0%, rgba(76, 76, 76, 1) 50%); + background: -o-linear-gradient(-45deg, rgba(86, 86, 86, 1) 0%, rgba(76, 76, 76, 1) 50%); + background: -ms-linear-gradient(-45deg, rgba(86, 86, 86, 1) 0%, rgba(76, 76, 76, 1) 50%); + background: linear-gradient(135deg, rgba(86, 86, 86, 1) 0%, rgba(76, 76, 76, 1) 50%); + box-shadow: 0px -1px 2px 0px rgba(0, 0, 0, 0.15); + -moz-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -o-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} +@-webkit-keyframes sparkle { + from { + background-position: 0 0; + } + to { + background-position: 0 -64px; + } +} +@-moz-keyframes sparkle { + from { + background-position: 0 0; + } + to { + background-position: 0 -64px; + } +} +@-o-keyframes sparkle { + from { + background-position: 0 0; + } + to { + background-position: 0 -64px; + } +} +@keyframes sparkle { + from { + background-position: 0 0; + } + to { + background-position: 0 -64px; + } +} diff --git a/ui/opening/package.json b/ui/opening/package.json index 4d7feb93fd..496f7d1a8b 100644 --- a/ui/opening/package.json +++ b/ui/opening/package.json @@ -30,9 +30,10 @@ "watchify": "^1.0.2" }, "dependencies": { - "chessground": "1.8.0", + "chessground": "1.8.7", + "chessli.js": "file:../chessli", "lodash-node": "^2.4.1", "merge": "^1.2.0", - "mithril": "0.1.24" + "mithril": "0.1.28" } } diff --git a/ui/opening/src/ctrl.js b/ui/opening/src/ctrl.js index 0e71be001b..f7b7a0d6d0 100644 --- a/ui/opening/src/ctrl.js +++ b/ui/opening/src/ctrl.js @@ -1,23 +1,95 @@ var m = require('mithril'); var chessground = require('chessground'); +var Chess = require('chessli.js').Chess; module.exports = function(cfg, router, i18n) { this.data = cfg; + console.log(this.data); + + this.vm = { + nbGood: this.data.opening.moves.filter(function(m) { + return m.quality === 'good'; + }).length, + figuredOut: [], + messedUp: [], + flash: null + }; + + var chess = new Chess(this.data.opening.fen); + var init = { + dests: chess.dests(), + check: chess.in_check() + }; + + var onMove = function(orig, dest, meta) { + submitMove(orig + dest); + setTimeout(function() { + this.chessground.set({ + fen: this.data.opening.fen, + lastMove: null, + turnColor: this.data.opening.color, + check: init.check, + premovable: { + enabled: false + }, + movable: { + dests: init.dests + } + }); + }.bind(this), 1000); + m.redraw(); + }.bind(this); this.chessground = new chessground.controller({ fen: this.data.opening.fen, orientation: this.data.opening.color, - viewOnly: true, + turnColor: this.data.opening.color, + check: init.check, + movable: { + color: this.data.opening.color, + free: false, + dests: init.dests, + events: { + after: onMove + } + }, }); + var submitMove = function(move) { + var found = this.data.opening.moves.filter(function(m) { + return m.first === move; + })[0]; + if (found && found.quality === 'good') { + if (this.vm.figuredOut.indexOf(move) === -1) this.vm.figuredOut.push(move); + } else if (found && found.quality === 'dubious') { + flash('dubious'); + } else if (!found || found.quality === 'bad') { + if (this.vm.messedUp.indexOf(move) === -1) this.vm.messedUp.push(move); + flash('bad'); + } + }.bind(this); + + var flash = function(f) { + this.vm.flash = f; + setTimeout(function() { + this.vm.flash = null; + m.redraw(); + }.bind(this), 1000); + }.bind(this); + this.router = router; this.trans = function() { - var str = i18n[arguments[0]] + var str = i18n[arguments[0]] || untranslated[arguments[0]] || arguments[0]; Array.prototype.slice.call(arguments, 1).forEach(function(arg) { str = str.replace('%s', arg); }); return str; }; + + var untranslated = { + yourOpeningScoreX: 'Your opening score: %s', + findNbGoodMoves: 'Find %s good moves', + }; }; diff --git a/ui/opening/src/main.js b/ui/opening/src/main.js index 5df733cefe..7c46982d3c 100644 --- a/ui/opening/src/main.js +++ b/ui/opening/src/main.js @@ -1,3 +1,4 @@ +var m = require('mithril'); var ctrl = require('./ctrl'); var view = require('./view'); @@ -7,5 +8,8 @@ module.exports = function(element, config, router, i18n) { controller: function () { return controller; }, view: view }); - controller.initiate(); }; + +// lol, that's for the rest of lichess to access mithril +// without having to include it a second time +window.Chessground = require('chessground'); diff --git a/ui/opening/src/view.js b/ui/opening/src/view.js index 3be4a6f78f..8d296b63a1 100644 --- a/ui/opening/src/view.js +++ b/ui/opening/src/view.js @@ -1,5 +1,9 @@ -var chessground = require('chessground'); var m = require('mithril'); +var chessground = require('chessground'); + +function strong(txt) { + return '' + txt + ''; +} function renderTable(ctrl) { return m('div.table', @@ -7,13 +11,101 @@ function renderTable(ctrl) { ); } +function renderUserInfos(ctrl) { + return m('div.chart_container', [ + m('p', m.trust(ctrl.trans('yourOpeningScoreX', strong(ctrl.data.user.score)))), + ctrl.data.user.history ? m('div.user_chart', { + config: function(el, isUpdate, context) { + var hash = ctrl.data.user.history.join(''); + if (hash == context.hash) return; + context.hash = hash; + var dark = document.body.classList.contains('dark'); + jQuery(el).sparkline(ctrl.data.user.history, { + type: 'line', + width: '213px', + height: '80px', + lineColor: dark ? '#4444ff' : '#0000ff', + fillColor: dark ? '#222255' : '#ccccff' + }); + } + }) : null + ]); +} + +function renderTrainingBox(ctrl) { + return m('div.box', [ + m('h1', ctrl.trans('training')), + m('div.tabs.buttonset', [ + m('a.button', { + href: '/training' + }, 'Puzzle'), + m('a.button', { + href: '/training/coordinate' + }, 'Coord'), + m('a.button.active', { + href: '/training/opening' + }, 'Opening'), + ]), + ctrl.data.user ? renderUserInfos(ctrl) : m('div.register', [ + m('p', ctrl.trans('toTrackYourProgress')), + m('p.signup', + m('a.button', { + href: '/signup', + }, ctrl.trans('signUp')) + ), + m('p', ctrl.trans('trainingSignupExplanation')) + ]) + ]); +} + +function renderResult(ctrl) { + return [ + m('div.goal', m.trust(ctrl.trans('findNbGoodMoves', strong(ctrl.vm.nbGood)))), + ]; +} + +function renderSide(ctrl) { + return m('div.side', [ + renderTrainingBox(ctrl), + renderResult(ctrl) + ]); +} + module.exports = function(ctrl) { + var percent = Math.ceil(ctrl.vm.figuredOut.length * 100 / ctrl.vm.nbGood) + '%'; + var color; + switch (ctrl.vm.flash) { + case 'dubious': + color = 'yellow'; + break; + case 'bad': + color = 'red'; + break; + default: + color = 'green'; + } return m('div#opening.training', [ + renderSide(ctrl), m('div.board_and_ground', [ m('div', chessground.view(ctrl.chessground)), m('div.right', renderTable(ctrl)) ]), m('div.center', [ + m('div.lulzbar', [ + m('div.bar.' + color, { + style: { + width: percent + } + }, m('span')), + m('div.label', { + style: { + left: percent + } + }, [ + m('span'), + m('div.perc', percent) + ]) + ]) ]) ]); }; diff --git a/ui/puzzle/src/view.js b/ui/puzzle/src/view.js index ad59a32f2f..9417591aed 100644 --- a/ui/puzzle/src/view.js +++ b/ui/puzzle/src/view.js @@ -40,7 +40,7 @@ function renderTrainingBox(ctrl) { }, 'Puzzle'), m('a.button', { href: '/training/coordinate' - }, 'Coordinate') + }, 'Coord') ]), ctrl.data.user ? renderUserInfos(ctrl) : m('div.register', [ m('p', ctrl.trans('toTrackYourProgress')),