puzzle replay WIP
parent
da24960403
commit
97525249d6
|
@ -10,7 +10,7 @@ import lila.app._
|
|||
import lila.common.ApiVersion
|
||||
import lila.common.config.MaxPerSecond
|
||||
import lila.puzzle.PuzzleTheme
|
||||
import lila.puzzle.{ Result, PuzzleRound, PuzzleDifficulty, Puzzle => Puz }
|
||||
import lila.puzzle.{ Result, PuzzleRound, PuzzleDifficulty, PuzzleReplay, Puzzle => Puz }
|
||||
|
||||
final class Puzzle(
|
||||
env: Env,
|
||||
|
@ -20,6 +20,7 @@ final class Puzzle(
|
|||
private def renderJson(
|
||||
puzzle: Puz,
|
||||
theme: PuzzleTheme,
|
||||
replay: Option[PuzzleReplay] = None,
|
||||
newUser: Option[lila.user.User] = None,
|
||||
apiVersion: Option[ApiVersion] = None
|
||||
)(implicit
|
||||
|
@ -28,10 +29,12 @@ final class Puzzle(
|
|||
if (apiVersion.exists(!_.puzzleV2))
|
||||
env.puzzle.jsonView.bc(puzzle = puzzle, theme = theme, user = newUser orElse ctx.me)
|
||||
else
|
||||
env.puzzle.jsonView(puzzle = puzzle, theme = theme, user = newUser orElse ctx.me)
|
||||
env.puzzle.jsonView(puzzle = puzzle, theme = theme, replay = replay, user = newUser orElse ctx.me)
|
||||
|
||||
private def renderShow(puzzle: Puz, theme: PuzzleTheme)(implicit ctx: Context) =
|
||||
renderJson(puzzle, theme) zip
|
||||
private def renderShow(puzzle: Puz, theme: PuzzleTheme, replay: Option[PuzzleReplay] = None)(implicit
|
||||
ctx: Context
|
||||
) =
|
||||
renderJson(puzzle, theme, replay) zip
|
||||
ctx.me.??(u => env.puzzle.session.getDifficulty(u) dmap some) map { case (json, difficulty) =>
|
||||
EnableSharedArrayBuffer(
|
||||
Ok(views.html.puzzle.show(puzzle, json, env.puzzle.jsonView.pref(ctx.pref), difficulty))
|
||||
|
@ -118,7 +121,7 @@ final class Puzzle(
|
|||
else
|
||||
for {
|
||||
next <- nextPuzzleForMe(theme.key)
|
||||
nextJson <- renderJson(next, theme, newUser.some)
|
||||
nextJson <- renderJson(next, theme, none, newUser.some)
|
||||
} yield Json.obj(
|
||||
"round" -> env.puzzle.jsonView.roundJson(me, round, perf),
|
||||
"next" -> nextJson
|
||||
|
@ -253,6 +256,15 @@ final class Puzzle(
|
|||
}
|
||||
}
|
||||
|
||||
def replay(days: Int, themeKey: String) =
|
||||
Auth { implicit ctx => me =>
|
||||
val theme = PuzzleTheme.findOrAny(themeKey)
|
||||
env.puzzle.replay(me, days, theme.key) flatMap {
|
||||
case None => Redirect(routes.Puzzle.dashboard(days)).fuccess
|
||||
case Some((puzzle, replay)) => renderShow(puzzle, theme, replay.some)
|
||||
}
|
||||
}
|
||||
|
||||
def mobileBcLoad(nid: Long) =
|
||||
Open { implicit ctx =>
|
||||
negotiate(
|
||||
|
|
|
@ -49,21 +49,21 @@ object dashboard {
|
|||
case Some(dash) =>
|
||||
frag(
|
||||
div(cls := s"${baseClass}__global")(
|
||||
metricsOf(dash.global)
|
||||
metricsOf(days, PuzzleTheme.mix.key, dash.global)
|
||||
),
|
||||
div(cls := s"${baseClass}__themes")(
|
||||
div(cls := s"${baseClass}__themes__title")(
|
||||
h2("Your weaknesses"),
|
||||
p("Train these to optimize your progress!")
|
||||
),
|
||||
themeSelection(dash weakestThemes 5)
|
||||
themeSelection(days, dash weakestThemes 5)
|
||||
),
|
||||
div(cls := s"${baseClass}__themes")(
|
||||
div(cls := s"${baseClass}__themes__title")(
|
||||
h2("Your strengths"),
|
||||
p("Congratulations, you did really well in these puzzles!")
|
||||
),
|
||||
themeSelection(dash strongestThemes 5)
|
||||
themeSelection(days, dash strongestThemes 5)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -71,7 +71,9 @@ object dashboard {
|
|||
)
|
||||
}
|
||||
|
||||
private def themeSelection(themes: List[(PuzzleTheme.Key, PuzzleDashboard.Results)])(implicit lang: Lang) =
|
||||
private def themeSelection(days: Int, themes: List[(PuzzleTheme.Key, PuzzleDashboard.Results)])(implicit
|
||||
lang: Lang
|
||||
) =
|
||||
themes.map { case (key, results) =>
|
||||
div(cls := themeClass)(
|
||||
div(cls := s"${themeClass}__meta")(
|
||||
|
@ -80,18 +82,20 @@ object dashboard {
|
|||
),
|
||||
p(cls := s"${themeClass}__description")(PuzzleTheme(key).description())
|
||||
),
|
||||
metricsOf(results)
|
||||
metricsOf(days, key, results)
|
||||
)
|
||||
}
|
||||
|
||||
private def metricsOf(results: PuzzleDashboard.Results)(implicit lang: Lang) =
|
||||
private def metricsOf(days: Int, theme: PuzzleTheme.Key, results: PuzzleDashboard.Results)(implicit
|
||||
lang: Lang
|
||||
) =
|
||||
div(cls := s"${baseClass}__metrics")(
|
||||
div(cls := s"$metricClass $metricClass--played")(
|
||||
strong(results.nb.localize),
|
||||
span("played")
|
||||
),
|
||||
div(cls := s"$metricClass $metricClass--perf")(
|
||||
strong(results.performance),
|
||||
strong(results.performance, results.unclear ?? "?"),
|
||||
span("performance")
|
||||
),
|
||||
div(
|
||||
|
@ -101,7 +105,7 @@ object dashboard {
|
|||
strong(s"${results.winPercent}%"),
|
||||
span("solved")
|
||||
),
|
||||
div(cls := s"$metricClass $metricClass--fix")(
|
||||
a(cls := s"$metricClass $metricClass--fix", href := routes.Puzzle.replay(days, theme.value))(
|
||||
strong(results.unfixed),
|
||||
span("to replay")
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package views.html.puzzle
|
||||
|
||||
import controllers.routes
|
||||
import play.api.libs.json.{ JsObject, Json }
|
||||
|
||||
import lila.api.Context
|
||||
|
@ -7,8 +8,6 @@ import lila.app.templating.Environment._
|
|||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.common.String.html.safeJsonValue
|
||||
|
||||
import controllers.routes
|
||||
|
||||
object show {
|
||||
|
||||
def apply(
|
||||
|
|
|
@ -82,8 +82,9 @@ GET /training/daily controllers.Puzzle.daily
|
|||
GET /training/frame controllers.Puzzle.frame
|
||||
GET /training/export/gif/thumbnail/:id.gif controllers.Export.puzzleThumbnail(id: String)
|
||||
GET /training/themes controllers.Puzzle.themes
|
||||
GET /training/dashboard controllers.Puzzle.dashboard(days: Int = 7)
|
||||
GET /training/dashboard controllers.Puzzle.dashboard(days: Int = 30)
|
||||
GET /training/dashboard/$days<\d+> controllers.Puzzle.dashboard(days: Int)
|
||||
GET /training/replay/$days<\d+>/:theme controllers.Puzzle.replay(days: Int, theme: String)
|
||||
GET /training/batch controllers.Puzzle.mobileBcBatchSelect
|
||||
POST /training/batch controllers.Puzzle.mobileBcBatchSolve
|
||||
GET /training/new controllers.Puzzle.mobileBcNew
|
||||
|
|
|
@ -76,6 +76,8 @@ final class Env(
|
|||
|
||||
lazy val dashboard = wire[PuzzleDashboardApi]
|
||||
|
||||
lazy val replay = wire[PuzzleReplayApi]
|
||||
|
||||
def cli =
|
||||
new lila.common.Cli {
|
||||
def process = { case "puzzle" :: "delete" :: id :: Nil =>
|
||||
|
|
|
@ -17,7 +17,7 @@ final class JsonView(
|
|||
|
||||
import JsonView._
|
||||
|
||||
def apply(puzzle: Puzzle, theme: PuzzleTheme, user: Option[User])(implicit
|
||||
def apply(puzzle: Puzzle, theme: PuzzleTheme, replay: Option[PuzzleReplay], user: Option[User])(implicit
|
||||
lang: Lang
|
||||
): Fu[JsObject] = {
|
||||
gameJson(
|
||||
|
@ -38,6 +38,7 @@ final class JsonView(
|
|||
.add("chapter" -> PuzzleTheme.studyChapterIds.get(theme.key))
|
||||
)
|
||||
.add("user" -> user.map(userJson))
|
||||
.add("replay" -> replay.map(replayJson))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +51,9 @@ final class JsonView(
|
|||
"provisional" -> u.perfs.puzzle.provisional
|
||||
)
|
||||
|
||||
private def replayJson(r: PuzzleReplay) =
|
||||
Json.obj("days" -> r.days, "i" -> r.i, "of" -> r.nb)
|
||||
|
||||
def roundJson(u: User, round: PuzzleRound, perf: Perf) =
|
||||
Json
|
||||
.obj(
|
||||
|
|
|
@ -123,7 +123,7 @@ final class PuzzleDashboardApi(
|
|||
|
||||
private def countField(field: String) = $doc("$cond" -> $arr("$" + field, 1, 0))
|
||||
|
||||
private val puzzleLookup =
|
||||
private[puzzle] val puzzleLookup =
|
||||
$doc(
|
||||
"$lookup" -> $doc(
|
||||
"from" -> colls.puzzle.name.value,
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
package lila.puzzle
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import reactivemongo.api.bson.BSONNull
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.chaining._
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi
|
||||
import lila.user.User
|
||||
|
||||
case class PuzzleReplay(days: Int, theme: PuzzleTheme.Key, nb: Int, remaining: Vector[Puzzle.Id]) {
|
||||
|
||||
def i = nb - remaining.size
|
||||
|
||||
def step = remaining.headOption map { _ -> copy(remaining = remaining drop 1) }
|
||||
}
|
||||
|
||||
final class PuzzleReplayApi(
|
||||
colls: PuzzleColls,
|
||||
cacheApi: CacheApi
|
||||
)(implicit ec: ExecutionContext) {
|
||||
|
||||
import BsonHandlers._
|
||||
|
||||
private val maxPuzzles = 100
|
||||
|
||||
private val replays = cacheApi.notLoading[User.ID, PuzzleReplay](512, "puzzle.replay")(
|
||||
_.expireAfterWrite(1 hour).buildAsync()
|
||||
)
|
||||
|
||||
def apply(user: User, days: Int, theme: PuzzleTheme.Key): Fu[Option[(Puzzle, PuzzleReplay)]] =
|
||||
replays.getFuture(user.id, _ => createReplayFor(user, days, theme)) flatMap { current =>
|
||||
if (current.days == days && current.theme == theme) fuccess(current)
|
||||
else createReplayFor(user, days, theme) tap { replays.put(user.id, _) }
|
||||
} flatMap {
|
||||
_.step ?? { case (puzzleId, newReplay) =>
|
||||
replays.put(user.id, fuccess(newReplay))
|
||||
colls.puzzle(_.byId[Puzzle](puzzleId.value)) map2 (_ -> newReplay)
|
||||
}
|
||||
}
|
||||
|
||||
private def createReplayFor(user: User, days: Int, theme: PuzzleTheme.Key): Fu[PuzzleReplay] =
|
||||
colls
|
||||
.round {
|
||||
_.aggregateOne() { framework =>
|
||||
import framework._
|
||||
Match(
|
||||
$doc(
|
||||
"u" -> user.id,
|
||||
"d" $gt DateTime.now.minusDays(days),
|
||||
"w" $ne true
|
||||
)
|
||||
) -> List(
|
||||
Sort(Ascending("d")),
|
||||
PipelineOperator(
|
||||
$doc(
|
||||
"$lookup" -> $doc(
|
||||
"from" -> colls.puzzle.name.value,
|
||||
"as" -> "puzzle",
|
||||
"let" -> $doc(
|
||||
"pid" -> $doc("$arrayElemAt" -> $arr($doc("$split" -> $arr("$_id", ":")), 1))
|
||||
),
|
||||
"pipeline" -> $arr(
|
||||
$doc(
|
||||
"$match" -> $doc(
|
||||
"$expr" -> {
|
||||
if (theme == PuzzleTheme.mix.key) $doc("$eq" -> $arr("$_id", "$$pid"))
|
||||
else
|
||||
$doc(
|
||||
"$and" -> $arr(
|
||||
$doc("$eq" -> $arr("$_id", "$$pid")),
|
||||
$doc("$in" -> $arr(theme, "$themes"))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
$doc("$limit" -> maxPuzzles),
|
||||
$doc("$project" -> $doc("_id" -> true))
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Unwind("puzzle"),
|
||||
Group(BSONNull)("ids" -> PushField("puzzle._id"))
|
||||
)
|
||||
}
|
||||
}
|
||||
.map {
|
||||
~_.flatMap(_.getAsOpt[Vector[Puzzle.Id]]("ids"))
|
||||
} map { ids =>
|
||||
PuzzleReplay(days, theme, ids.size, ids)
|
||||
}
|
||||
|
||||
private val puzzleLookup =
|
||||
$doc(
|
||||
"$lookup" -> $doc(
|
||||
"from" -> colls.puzzle.name.value,
|
||||
"as" -> "puzzle",
|
||||
"let" -> $doc(
|
||||
"pid" -> $doc("$arrayElemAt" -> $arr($doc("$split" -> $arr("$_id", ":")), 1))
|
||||
),
|
||||
"pipeline" -> $arr(
|
||||
$doc(
|
||||
"$match" -> $doc(
|
||||
"$expr" -> $doc(
|
||||
$doc("$eq" -> $arr("$_id", "$$pid"))
|
||||
)
|
||||
)
|
||||
),
|
||||
$doc("$project" -> $doc("_id" -> true))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
|
@ -29,11 +29,7 @@ final class PuzzleSessionApi(
|
|||
pathApi: PuzzlePathApi,
|
||||
cacheApi: CacheApi,
|
||||
userRepo: UserRepo
|
||||
)(implicit
|
||||
ec: ExecutionContext,
|
||||
system: akka.actor.ActorSystem,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
)(implicit ec: ExecutionContext) {
|
||||
|
||||
import BsonHandlers._
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
|
||||
&__global .puzzle-dashboard__metrics__metric {
|
||||
padding: 1em 0 1.5em 0;
|
||||
border-radius: 6px;
|
||||
border-radius: 7px;
|
||||
|
||||
strong {
|
||||
font-size: 5em;
|
||||
|
|
|
@ -54,25 +54,31 @@
|
|||
|
||||
|
||||
@include breakpoint($mq-col3) {
|
||||
grid-template-areas:
|
||||
'side . board gauge tools'
|
||||
'side . session . controls'
|
||||
'side . . . .';
|
||||
grid-template-areas: 'side . board gauge tools' 'side . session . controls' 'side . . . .';
|
||||
grid-template-columns: $col3-uniboard-side $block-gap $col3-uniboard-width var(--gauge-gap) $col3-uniboard-table;
|
||||
}
|
||||
|
||||
|
||||
&__side {
|
||||
display: grid;
|
||||
grid-template-areas: 'user' 'theme' 'metas' 'config';
|
||||
grid-gap: $block-gap;
|
||||
grid-template-areas: 'user' 'theme' 'metas' 'config';
|
||||
|
||||
.puzzle-replay & {
|
||||
grid-template-areas: 'replay' 'user' 'metas' 'config';
|
||||
}
|
||||
|
||||
@include breakpoint($mq-xx-small) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas: 'metas user' 'metas theme' 'config theme' '. theme';
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
|
||||
.puzzle-replay & {
|
||||
grid-template-areas: 'metas replay' 'metas user' 'config user';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@include breakpoint($mq-x-large) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas: 'metas' 'user' 'theme' 'config';
|
||||
|
@ -80,8 +86,13 @@
|
|||
justify-self: end;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
|
||||
.puzzle-replay & {
|
||||
grid-template-areas: 'metas' 'replay' 'user' 'config';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__metas {
|
||||
grid-area: metas;
|
||||
}
|
||||
|
@ -97,5 +108,9 @@
|
|||
&__theme {
|
||||
grid-area: theme;
|
||||
}
|
||||
|
||||
&__replay {
|
||||
grid-area: replay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,9 +45,7 @@
|
|||
padding: 2vmin;
|
||||
|
||||
&__rating {
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
@extend %flex-center;
|
||||
|
||||
|
@ -101,6 +99,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__replay {
|
||||
@extend %box-neat;
|
||||
|
||||
background: $c-bg-box;
|
||||
padding: 2vmin;
|
||||
|
||||
a {
|
||||
@extend %roboto;
|
||||
|
||||
font-size: 1.3em;
|
||||
margin-bottom: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
@extend %box-radius, %flex-center;
|
||||
|
||||
justify-content: center;
|
||||
padding: 0 1em;
|
||||
height: 2.5em;
|
||||
|
||||
$c-bar-bg: $c-bg-zebra2;
|
||||
$c-bar-fg: mix($c-bg-page, $c-primary, 40%);
|
||||
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to right, $c-bar-fg 0%, $c-bar-fg var(--p), $c-bar-bg var(--p), $c-bar-bg 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&__theme {
|
||||
@extend %box-neat, %roboto;
|
||||
|
||||
|
@ -192,8 +219,8 @@
|
|||
|
||||
&__lock {
|
||||
@extend %flex-center;
|
||||
justify-content: center;
|
||||
|
||||
justify-content: center;
|
||||
flex: 0 0 100%;
|
||||
color: $c-font-dimmer;
|
||||
opacity: .5;
|
||||
|
@ -202,6 +229,7 @@
|
|||
&__selector {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
&__study {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -127,6 +127,13 @@ export interface PuzzleData {
|
|||
theme: Theme;
|
||||
game: PuzzleGame;
|
||||
user: PuzzleUser | undefined;
|
||||
replay?: PuzzleReplay;
|
||||
}
|
||||
|
||||
export interface PuzzleReplay {
|
||||
i: number;
|
||||
of: number;
|
||||
days: number;
|
||||
}
|
||||
|
||||
export interface PuzzleGame {
|
||||
|
|
|
@ -75,7 +75,7 @@ export default function(ctrl: Controller): VNode {
|
|||
if (!cevalShown) ctrl.vm.autoScrollNow = true;
|
||||
cevalShown = showCeval;
|
||||
}
|
||||
return h('main.puzzle', {
|
||||
return h(`main.puzzle.puzzle-${ctrl.getData().replay ? 'replay' : 'play'}`, {
|
||||
class: { 'gauge-on': gaugeOn },
|
||||
hook: {
|
||||
postpatch(old, vnode) {
|
||||
|
@ -91,6 +91,7 @@ export default function(ctrl: Controller): VNode {
|
|||
}
|
||||
}, [
|
||||
h('aside.puzzle__side', [
|
||||
side.replay(ctrl),
|
||||
side.puzzleBox(ctrl),
|
||||
side.userBox(ctrl),
|
||||
side.config(ctrl),
|
||||
|
|
|
@ -56,7 +56,7 @@ export function userBox(ctrl: Controller): VNode {
|
|||
]);
|
||||
const diff = ctrl.vm.round?.ratingDiff;
|
||||
return h('div.puzzle__side__user', [
|
||||
h('p.puzzle__side__user__rating', ctrl.trans.vdom('yourPuzzleRatingX', h('strong', [
|
||||
h('div.puzzle__side__user__rating', ctrl.trans.vdom('yourPuzzleRatingX', h('strong', [
|
||||
data.user.rating - (diff || 0),
|
||||
...(diff && diff > 0 ? [' ', h('good.rp', '+' + diff)] : []),
|
||||
...(diff && diff < 0 ? [' ', h('bad.rp', '−' + (-diff))] : [])
|
||||
|
@ -66,8 +66,24 @@ export function userBox(ctrl: Controller): VNode {
|
|||
|
||||
const difficulties: PuzzleDifficulty[] = ['easiest', 'easier', 'normal', 'harder', 'hardest'];
|
||||
|
||||
export function config(ctrl: Controller): MaybeVNode {
|
||||
export function replay(ctrl: Controller): MaybeVNode {
|
||||
const replay = ctrl.getData().replay;
|
||||
return replay ?
|
||||
h('div.puzzle__side__replay', [
|
||||
h('a', {
|
||||
attrs: {
|
||||
href: `/training/dashboard/${replay.days}`
|
||||
}
|
||||
}, ['« ', `Replaying ${ctrl.trans.noarg(ctrl.getData().theme.key)} puzzles`]),
|
||||
h('div.puzzle__side__replay__bar', {
|
||||
attrs: {
|
||||
style: `--p:${Math.round(100 * replay.i / replay.of)}%`
|
||||
},
|
||||
}, `${replay.i} / ${replay.of}`)
|
||||
]) : null;
|
||||
}
|
||||
|
||||
export function config(ctrl: Controller): MaybeVNode {
|
||||
const id = 'puzzle-toggle-autonext';
|
||||
return h('div.puzzle__side__config', [
|
||||
h('div.puzzle__side__config__jump', [
|
||||
|
@ -86,7 +102,7 @@ export function config(ctrl: Controller): MaybeVNode {
|
|||
]),
|
||||
h('label', { attrs: { 'for': id } }, ctrl.trans.noarg('jumpToNextPuzzleImmediately'))
|
||||
]),
|
||||
ctrl.difficulty ? h('form.puzzle__side__config__difficulty', {
|
||||
!ctrl.getData().replay && ctrl.difficulty ? h('form.puzzle__side__config__difficulty', {
|
||||
attrs: {
|
||||
action: `/training/difficulty/${ctrl.getData().theme.key}`,
|
||||
method: 'post'
|
||||
|
@ -107,5 +123,6 @@ export function config(ctrl: Controller): MaybeVNode {
|
|||
}, ctrl.trans.noarg(diff))
|
||||
))
|
||||
]) : null
|
||||
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { bind, dataIcon } from '../util';
|
||||
import { Controller } from '../interfaces';
|
||||
import { Controller, MaybeVNode } from '../interfaces';
|
||||
import { h } from 'snabbdom';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
|
||||
const studyUrl = 'https://lichess.org/study/viiWlKjv';
|
||||
|
||||
export default function theme(ctrl: Controller): VNode {
|
||||
export default function theme(ctrl: Controller): MaybeVNode {
|
||||
const t = ctrl.getData().theme;
|
||||
return h('div.puzzle__side__theme', [
|
||||
return ctrl.getData().replay ? null : h('div.puzzle__side__theme', [
|
||||
h('a', { attrs: { href: '/training/themes' } }, h('h2', ['« ', t.name])),
|
||||
h('p', [
|
||||
t.desc,
|
||||
|
|
Loading…
Reference in New Issue