more puzzle WIP
parent
a86be49e10
commit
11c0bdfd51
|
@ -126,7 +126,7 @@ final class Puzzle(
|
|||
def voteTheme(id: String, themeStr: String) =
|
||||
AuthBody { implicit ctx => me =>
|
||||
NoBot {
|
||||
PuzzleTheme.find(themeStr) ?? { theme =>
|
||||
PuzzleTheme.findDynamic(themeStr) ?? { theme =>
|
||||
implicit val req = ctx.body
|
||||
env.puzzle.forms.themeVote
|
||||
.bindFromRequest()
|
||||
|
@ -208,7 +208,10 @@ final class Puzzle(
|
|||
def showWithTheme(themeKey: String, id: String) = Open { implicit ctx =>
|
||||
NoBot {
|
||||
val theme = PuzzleTheme.findOrAny(themeKey)
|
||||
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { renderShow(_, theme) }
|
||||
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { puzzle =>
|
||||
if (puzzle.themes contains theme.key) renderShow(puzzle, theme)
|
||||
else Redirect(routes.Puzzle.show(puzzle.id.value)).fuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ package views
|
|||
package html.puzzle
|
||||
|
||||
import play.api.i18n.Lang
|
||||
import play.api.libs.json.{ JsObject, Json }
|
||||
import play.api.libs.json.{ JsArray, JsObject, JsString, Json }
|
||||
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.i18n.JsDump
|
||||
import lila.i18n.MessageKey
|
||||
import lila.puzzle.PuzzleTheme
|
||||
|
||||
|
@ -18,11 +19,17 @@ object bits {
|
|||
|
||||
def jsI18n(implicit lang: Lang) = i18nJsObject(i18nKeys)
|
||||
|
||||
lazy val jsonThemes = JsObject(
|
||||
PuzzleTheme.categorized.map { case (name, themes) =>
|
||||
name.key -> Json.arr(themes.map(_.name.key))
|
||||
lazy val jsonThemes = PuzzleTheme.all
|
||||
.collect {
|
||||
case t if t != PuzzleTheme.any => t.key
|
||||
}
|
||||
)
|
||||
.partition(PuzzleTheme.staticThemes.contains) match {
|
||||
case (static, dynamic) =>
|
||||
Json.obj(
|
||||
"dynamic" -> dynamic.map(_.value).sorted.mkString(" "),
|
||||
"static" -> static.map(_.value).mkString(" ")
|
||||
)
|
||||
}
|
||||
|
||||
private val i18nKeys: List[MessageKey] = {
|
||||
List(
|
||||
|
|
|
@ -18,12 +18,13 @@ object show {
|
|||
moreJs = frag(
|
||||
jsModule("puzzle"),
|
||||
embedJsUnsafeLoadThen(s"""LichessPuzzle(${safeJsonValue(
|
||||
Json.obj(
|
||||
"data" -> data,
|
||||
"pref" -> pref,
|
||||
"i18n" -> bits.jsI18n,
|
||||
"themes" -> bits.jsonThemes
|
||||
)
|
||||
Json
|
||||
.obj(
|
||||
"data" -> data,
|
||||
"pref" -> pref,
|
||||
"i18n" -> bits.jsI18n
|
||||
)
|
||||
.add("themes" -> ctx.isAuth.option(bits.jsonThemes))
|
||||
)})""")
|
||||
),
|
||||
csp = defaultCsp.withWebAssembly.some,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Only looks for puzzles with the `dirty` flag, and removes it.
|
||||
*
|
||||
* mongo <IP>:<PORT>/<DB> mongodb-puzzle-denormalize-themes.js
|
||||
*
|
||||
*
|
||||
* Must run on the puzzle database.
|
||||
* Should run every 5 minutes.
|
||||
* Should complete within 10 seconds.
|
||||
|
@ -15,9 +15,24 @@ const playColl = db.puzzle2_puzzle;
|
|||
const roundColl = db.puzzle2_round;
|
||||
|
||||
const staticThemes = new Set([
|
||||
"oneMove", "short", "long", "veryLong",
|
||||
"mateIn1", "mateIn2", "mateIn3", "mateIn4", "mateIn5",
|
||||
"enPassant"
|
||||
'bishopEndgame',
|
||||
'enPassant',
|
||||
'endgame',
|
||||
'knightEndgame',
|
||||
'long',
|
||||
'mateIn1',
|
||||
'mateIn2',
|
||||
'mateIn3',
|
||||
'mateIn4',
|
||||
'mateIn5',
|
||||
'middlegame',
|
||||
'oneMove',
|
||||
'opening',
|
||||
'pawnEndgame',
|
||||
'queenEndgame',
|
||||
'rookEndgame',
|
||||
'short',
|
||||
'veryLong'
|
||||
]);
|
||||
|
||||
playColl.find({ dirty: true }, { themes: true }).forEach(p => {
|
||||
|
@ -36,7 +51,7 @@ playColl.find({ dirty: true }, { themes: true }).forEach(p => {
|
|||
themeMap[theme] = x.v * signum + (themeMap[theme] || 0);
|
||||
});
|
||||
|
||||
const newThemes = new Set();
|
||||
const newThemes = new Set(oldThemes.filter(t => staticThemes.has(t)));
|
||||
Object.keys(themeMap).forEach(theme => {
|
||||
if (themeMap[theme] > 0) newThemes.add(theme);
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ const tiers = [
|
|||
|
||||
const acceptableVoteSelect = {
|
||||
vote: {
|
||||
$gt: -150
|
||||
$gt: -9150
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ object JsDump {
|
|||
|
||||
private type JsTrans = Iterable[(String, JsString)]
|
||||
|
||||
private def removeDbPrefix(key: MessageKey): String = {
|
||||
def removeDbPrefix(key: MessageKey): String = {
|
||||
val index = key.indexOf(':')
|
||||
if (index > 0) key.drop(index + 1) else key
|
||||
}
|
||||
|
|
|
@ -72,11 +72,11 @@ final private[puzzle] class PuzzleApi(
|
|||
def vote(user: User, id: Puzzle.Id, theme: PuzzleTheme.Key, vote: Option[Boolean]): Funit =
|
||||
round.find(user, id).thenPp flatMap {
|
||||
_ ?? { round =>
|
||||
round.themeVote(theme, vote.pp("vote")).pp ?? { newThemes =>
|
||||
round.themeVote(theme, vote) ?? { newThemes =>
|
||||
import PuzzleRound.{ BSONFields => F }
|
||||
val update =
|
||||
if (newThemes.isEmpty) $unset(F.themes, F.puzzle)
|
||||
else $set(F.themes -> newThemes, F.puzzle -> id)
|
||||
else $set(F.themes -> newThemes, F.puzzle -> id, F.weight -> 1)
|
||||
colls.round(_.update.one($id(round.id), update)) zip
|
||||
colls.puzzle(_.updateField($id(round.id.puzzleId), Puzzle.BSONFields.dirty, true)) void
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ object PuzzleRound {
|
|||
val vote = "v"
|
||||
val themes = "t"
|
||||
val puzzle = "p" // only if themes is set!
|
||||
val weight = "w"
|
||||
val weight = "e"
|
||||
val user = "u" // student denormalization
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ private case class PuzzleSession(
|
|||
positionInPath = 0
|
||||
)
|
||||
def next = copy(positionInPath = positionInPath + 1)
|
||||
|
||||
override def toString = s"$path:$positionInPath"
|
||||
}
|
||||
|
||||
final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: UserRepo)(implicit
|
||||
|
@ -49,15 +51,15 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
|
|||
continueOrCreateSessionFor(user, theme) flatMap { session =>
|
||||
import NextPuzzleResult._
|
||||
def switchPath(tier: PuzzleTier) =
|
||||
nextPathIdFor(user.id, theme, tier, session.previousPaths) flatMap {
|
||||
case None => fufail(s"No remaining puzzle path for ${user.id}")
|
||||
case Some(pathId) =>
|
||||
val newSession = session.switchTo(tier, pathId)
|
||||
sessions.put(user.id, fuccess(newSession))
|
||||
nextPuzzleFor(user, theme, retries = retries + 1)
|
||||
nextPathIdFor(user, theme, tier, session.previousPaths) orElse {
|
||||
session.previousPaths.nonEmpty ?? nextPathIdFor(user, theme, tier, Set.empty)
|
||||
} orFail s"No puzzle path for ${user.id}" flatMap { pathId =>
|
||||
val newSession = session.switchTo(tier, pathId)
|
||||
sessions.put(user.id, fuccess(newSession))
|
||||
nextPuzzleFor(user, theme, retries = retries + 1)
|
||||
}
|
||||
|
||||
nextPuzzleResult(user, session).thenPp(session.path.value) flatMap {
|
||||
nextPuzzleResult(user, session).thenPp(session.toString) flatMap {
|
||||
case PathMissing | PathEnded if retries < 10 => switchPath(session.tier)
|
||||
case PathMissing | PathEnded => fufail(s"Puzzle path missing or ended for ${user.id}")
|
||||
case PuzzleMissing(id) =>
|
||||
|
@ -111,7 +113,6 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
|
|||
)
|
||||
}.map { docOpt =>
|
||||
import NextPuzzleResult._
|
||||
// println(docOpt map lila.db.BSON.debug)
|
||||
docOpt.fold[NextPuzzleResult](PathMissing) { doc =>
|
||||
doc.getAsOpt[Puzzle.Id]("puzzleId").fold[NextPuzzleResult](PathEnded) { puzzleId =>
|
||||
doc
|
||||
|
@ -153,35 +154,33 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
|
|||
}
|
||||
|
||||
private def createSessionFor(user: User, theme: PuzzleTheme.Key): Fu[PuzzleSession] =
|
||||
nextPathIdFor(user.id, theme, PuzzleTier.Top, Set.empty)
|
||||
nextPathIdFor(user, theme, PuzzleTier.Top, Set.empty)
|
||||
.orFail(s"No puzzle path found for ${user.id}, theme: $theme")
|
||||
.dmap(pathId => PuzzleSession(theme, PuzzleTier.Top, pathId, 0))
|
||||
|
||||
private def nextPathIdFor(
|
||||
userId: User.ID,
|
||||
user: User,
|
||||
theme: PuzzleTheme.Key,
|
||||
tier: PuzzleTier,
|
||||
previousPaths: Set[PathId]
|
||||
): Fu[Option[PathId]] =
|
||||
userRepo.perfOf(userId, PerfType.Puzzle).dmap(_ | Perf.default) flatMap { perf =>
|
||||
colls.path {
|
||||
_.aggregateOne() { framework =>
|
||||
import framework._
|
||||
Match(
|
||||
$doc(
|
||||
"_id" ->
|
||||
$doc(
|
||||
"$regex" -> BSONRegex(s"^${theme}_${tier}", ""),
|
||||
$nin(previousPaths)
|
||||
),
|
||||
"min" $lte perf.glicko.rating,
|
||||
"max" $gt perf.glicko.rating
|
||||
)
|
||||
) -> List(
|
||||
Project($id(true)),
|
||||
Sample(1)
|
||||
colls.path {
|
||||
_.aggregateOne() { framework =>
|
||||
import framework._
|
||||
Match(
|
||||
$doc(
|
||||
"_id" ->
|
||||
$doc(
|
||||
"$regex" -> BSONRegex(s"^${theme}_${tier}", ""),
|
||||
$nin(previousPaths)
|
||||
),
|
||||
"min" $lte user.perfs.puzzle.glicko.rating,
|
||||
"max" $gt user.perfs.puzzle.glicko.rating
|
||||
)
|
||||
}.dmap(_.flatMap(_.getAsOpt[PathId]("_id")))
|
||||
}
|
||||
) -> List(
|
||||
Project($id(true)),
|
||||
Sample(1)
|
||||
)
|
||||
}.dmap(_.flatMap(_.getAsOpt[PathId]("_id")))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,9 +131,33 @@ object PuzzleTheme {
|
|||
t.key -> t
|
||||
}.toMap
|
||||
|
||||
// themes that can't be voted by players
|
||||
val staticThemes: Set[Key] = Set(
|
||||
bishopEndgame,
|
||||
enPassant,
|
||||
endgame,
|
||||
knightEndgame,
|
||||
long,
|
||||
mateIn1,
|
||||
mateIn2,
|
||||
mateIn3,
|
||||
mateIn4,
|
||||
mateIn5,
|
||||
middlegame,
|
||||
oneMove,
|
||||
opening,
|
||||
pawnEndgame,
|
||||
queenEndgame,
|
||||
rookEndgame,
|
||||
short,
|
||||
veryLong
|
||||
).map(_.key)
|
||||
|
||||
def find(key: String) = byKey get Key(key)
|
||||
|
||||
def findOrAny(key: String) = find(key) | any
|
||||
|
||||
def findDynamic(key: String) = find(key).filterNot(t => staticThemes(t.key))
|
||||
|
||||
implicit val keyIso = lila.common.Iso.string[Key](Key.apply, _.value)
|
||||
}
|
||||
|
|
|
@ -16,16 +16,16 @@ function selector(data: StudyPracticeData) {
|
|||
h('option', {
|
||||
attrs: { disabled: true, selected: true }
|
||||
}, 'Practice list'),
|
||||
...data.structure.map(function(section) {
|
||||
return h('optgroup', {
|
||||
...data.structure.map(section =>
|
||||
h('optgroup', {
|
||||
attrs: { label: section.name }
|
||||
}, section.studies.map(function(study) {
|
||||
return option(
|
||||
}, section.studies.map(study =>
|
||||
option(
|
||||
section.id + '/' + study.slug + '/' + study.id,
|
||||
'',
|
||||
study.name);
|
||||
}));
|
||||
})
|
||||
study.name)
|
||||
))
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
&__session {
|
||||
grid-area: session;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.eval-gauge {
|
||||
|
@ -53,8 +54,11 @@
|
|||
|
||||
|
||||
@include breakpoint($mq-col3) {
|
||||
grid-template-areas: 'side . board gauge tools' '. . session . controls';
|
||||
grid-template-areas:
|
||||
'side . board gauge tools'
|
||||
'side . session . controls';
|
||||
grid-template-columns: $col3-uniboard-side $block-gap $col3-uniboard-width var(--gauge-gap) $col3-uniboard-table;
|
||||
grid-template-rows: $col3-uniboard-width;
|
||||
}
|
||||
|
||||
|
||||
|
@ -65,13 +69,14 @@
|
|||
|
||||
@include breakpoint($mq-xx-small) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas: 'metas user' 'metas theme' 'config theme';
|
||||
grid-template-areas: 'metas user' 'metas theme' 'config theme' '. theme';
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
}
|
||||
|
||||
@include breakpoint($mq-x-large) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
grid-template-areas: 'metas' 'user' 'theme' 'config';
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
justify-self: end;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
|
|
|
@ -122,12 +122,16 @@
|
|||
&:hover {
|
||||
background: mix($c-bg-box, $c-link, 90%);
|
||||
}
|
||||
|
||||
&.strike a {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__votes {
|
||||
@extend %flex-center-nowrap;
|
||||
flex: 0 1 5em;
|
||||
flex: 0 1 7em;
|
||||
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
|
@ -154,7 +158,7 @@
|
|||
@extend %data-icon;
|
||||
|
||||
content: 'h';
|
||||
font-size: 1.2em;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
&.vote-down::before {
|
||||
|
@ -172,4 +176,8 @@
|
|||
background: $c-bad;
|
||||
}
|
||||
}
|
||||
|
||||
&__selector {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { makeSanAndPlay } from 'chessops/san';
|
|||
import { parseFen, makeFen } from 'chessops/fen';
|
||||
import { parseSquare, parseUci, makeSquare, makeUci } from 'chessops/util';
|
||||
import { pgnToTree, mergeSolution } from './moveTree';
|
||||
import { Redraw, Vm, Controller, PuzzleOpts, PuzzleData, PuzzleResult, MoveTest } from './interfaces';
|
||||
import { Redraw, Vm, Controller, PuzzleOpts, PuzzleData, PuzzleResult, MoveTest, ThemeKey } from './interfaces';
|
||||
import { Role, Move, Outcome } from 'chessops/types';
|
||||
import { storedProp } from 'common/storage';
|
||||
import PuzzleSession from './session';
|
||||
|
@ -389,18 +389,22 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
|
|||
startCeval();
|
||||
}
|
||||
|
||||
const vote = throttle(1000, v => {
|
||||
const vote = throttle(1000, (v: boolean) => {
|
||||
xhr.vote(data.puzzle.id, v);
|
||||
nextPuzzle();
|
||||
});
|
||||
|
||||
const voteTheme = throttle(500, (theme, v) => {
|
||||
const voteTheme = (theme: ThemeKey, v: boolean) => {
|
||||
if (vm.round) {
|
||||
vm.round.themes = vm.round.themes || {};
|
||||
vm.round.themes[theme] = v;
|
||||
if (v || data.puzzle.themes.includes(theme))
|
||||
vm.round.themes[theme] = v;
|
||||
else
|
||||
delete vm.round.themes[theme];
|
||||
xhr.voteTheme(data.puzzle.id, theme, v);
|
||||
redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initiate(opts.data);
|
||||
|
||||
|
@ -473,6 +477,10 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
|
|||
redraw,
|
||||
ongoing: false,
|
||||
playBestMove,
|
||||
session
|
||||
session,
|
||||
allThemes: opts.themes && {
|
||||
dynamic: opts.themes.dynamic.split(' '),
|
||||
static: new Set(opts.themes.static.split(' '))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import PuzzleSession from './session';
|
||||
import { Api as CgApi } from 'chessground/api';
|
||||
import { CevalCtrl, NodeEvals } from 'ceval';
|
||||
import { Config as CgConfig } from 'chessground/config';
|
||||
import { Outcome } from 'chessops/types';
|
||||
import { Prop } from 'common';
|
||||
import { Role, Move } from 'chessops/types';
|
||||
import { StoredBooleanProp } from 'common/storage';
|
||||
import { TreeWrapper } from 'tree';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
import { StoredBooleanProp } from 'common/storage';
|
||||
import PuzzleSession from './session';
|
||||
|
||||
export type MaybeVNode = VNode | string | null | undefined;
|
||||
export type MaybeVNodes = MaybeVNode[];
|
||||
|
@ -25,6 +25,10 @@ export interface KeyboardController {
|
|||
}
|
||||
|
||||
export type ThemeKey = string;
|
||||
export interface AllThemes {
|
||||
dynamic: ThemeKey[];
|
||||
static: Set<ThemeKey>;
|
||||
}
|
||||
|
||||
export interface Controller extends KeyboardController {
|
||||
nextNodeBest(): string | undefined;
|
||||
|
@ -53,6 +57,7 @@ export interface Controller extends KeyboardController {
|
|||
promotion: any;
|
||||
autoNext: StoredBooleanProp;
|
||||
session: PuzzleSession;
|
||||
allThemes?: AllThemes;
|
||||
|
||||
path?: Tree.Path;
|
||||
autoScrollRequested?: boolean;
|
||||
|
@ -84,6 +89,10 @@ export interface PuzzleOpts {
|
|||
pref: PuzzlePrefs;
|
||||
data: PuzzleData;
|
||||
i18n: { [key: string]: string | undefined };
|
||||
themes?: {
|
||||
dynamic: string;
|
||||
static: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PuzzlePrefs {
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
import { bind } from '../util';
|
||||
import { Controller } from '../interfaces';
|
||||
import { h } from 'snabbdom';
|
||||
import { VNode } from 'snabbdom/vnode';
|
||||
import { bind } from '../util';
|
||||
|
||||
const staticThemes = new Set([
|
||||
"enPassant",
|
||||
"long",
|
||||
"mateIn1",
|
||||
"mateIn2",
|
||||
"mateIn3",
|
||||
"mateIn4",
|
||||
"mateIn5",
|
||||
"oneMove",
|
||||
"short",
|
||||
"veryLong"
|
||||
]);
|
||||
|
||||
export default function theme(ctrl: Controller): VNode {
|
||||
return h('div.puzzle__side__theme', [
|
||||
|
@ -26,34 +13,68 @@ export default function theme(ctrl: Controller): VNode {
|
|||
|
||||
const editor = (ctrl: Controller): VNode => {
|
||||
const data = ctrl.getData(),
|
||||
user = data.user,
|
||||
themes = ctrl.vm.round?.themes || {};
|
||||
votedThemes = ctrl.vm.round?.themes || {};
|
||||
const visibleThemes: string[] = data.puzzle.themes.concat(
|
||||
Object.keys(votedThemes).filter(t => votedThemes[t] && !data.puzzle.themes.includes(t))
|
||||
).sort()
|
||||
return h('div.puzzle__themes', [
|
||||
h('div.puzzle__themes_list', {
|
||||
hook: bind('click', e => {
|
||||
const target = e.target as HTMLElement;
|
||||
const theme = target.getAttribute('data-theme');
|
||||
if (theme) ctrl.voteTheme(theme, target.classList.contains('vote-up'));
|
||||
}, ctrl.redraw)
|
||||
}, data.puzzle.themes.map(key =>
|
||||
h('div.puzzle__themes__list__entry', [
|
||||
const vote = target.classList.contains('vote-up');
|
||||
const votedThemes = ctrl.vm.round?.themes || {};
|
||||
if (theme && votedThemes[theme] !== vote) ctrl.voteTheme(theme, vote);
|
||||
})
|
||||
}, visibleThemes.map(key =>
|
||||
h('div.puzzle__themes__list__entry', {
|
||||
class: {
|
||||
strike: votedThemes[key] === false
|
||||
}
|
||||
}, [
|
||||
h('a', {
|
||||
attrs: {
|
||||
href: `/training/${key}`,
|
||||
title: ctrl.trans.noarg(`${key}Description`)
|
||||
}
|
||||
}, ctrl.trans.noarg(key)),
|
||||
!user || staticThemes.has(key) ? null : h('div.puzzle__themes__votes', [
|
||||
!ctrl.allThemes || ctrl.allThemes.static.has(key) ? null : h('div.puzzle__themes__votes', [
|
||||
h('span.puzzle__themes__vote.vote-up', {
|
||||
class: { active: themes[key] },
|
||||
class: { active: votedThemes[key] },
|
||||
attrs: { 'data-theme': key }
|
||||
}),
|
||||
h('span.puzzle__themes__vote.vote-down', {
|
||||
class: { active: themes[key] === false },
|
||||
class: { active: votedThemes[key] === false },
|
||||
attrs: { 'data-theme': key }
|
||||
})
|
||||
])
|
||||
])
|
||||
))
|
||||
)),
|
||||
ctrl.allThemes ? h('select.puzzle__themes__selector', {
|
||||
hook: {
|
||||
...bind('change', e => {
|
||||
const theme = (e.target as HTMLInputElement).value;
|
||||
if (theme) ctrl.voteTheme(theme, true);
|
||||
setTimeout(() => {
|
||||
((e.target as HTMLInputElement).parentNode as HTMLSelectElement).value = '';
|
||||
}, 500);
|
||||
}),
|
||||
postpatch(_, vnode) {
|
||||
(vnode.elm as HTMLSelectElement).value = '';
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('option', {
|
||||
attrs: { value: '', selected: true }
|
||||
}, 'Add another theme'),
|
||||
...ctrl.allThemes.dynamic.filter(t => !votedThemes[t]).map(theme =>
|
||||
h('option', {
|
||||
attrs: {
|
||||
value: theme,
|
||||
title: ctrl.trans.noarg(`${theme}Description`)
|
||||
},
|
||||
}, ctrl.trans.noarg(theme))
|
||||
)
|
||||
]) : null
|
||||
]);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue