puzzle dashboard WIP
parent
292dbcc9eb
commit
47a68eddbd
|
@ -3,10 +3,12 @@ package html.puzzle
|
|||
|
||||
import controllers.routes
|
||||
import play.api.i18n.Lang
|
||||
import play.api.libs.json.Json
|
||||
|
||||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.common.String.html.safeJsonValue
|
||||
import lila.puzzle.PuzzleDashboard
|
||||
import lila.puzzle.PuzzleTheme
|
||||
import lila.user.User
|
||||
|
@ -27,15 +29,55 @@ object dashboard {
|
|||
if (ctx is user) "Puzzle dashboard"
|
||||
else s"${user.username} puzzle dashboard",
|
||||
subtitle = "Let's see how good you've been doing",
|
||||
dashOpt = dashOpt
|
||||
dashOpt = dashOpt,
|
||||
moreJs = dashOpt ?? { dash =>
|
||||
val mostPlayed = dash.mostPlayed.sortBy { case (key, _) =>
|
||||
PuzzleTheme(key).name.txt()
|
||||
}
|
||||
frag(
|
||||
jsModule("puzzle.dashboard"),
|
||||
embedJsUnsafeLoadThen(s"""LichessPuzzleDashboard.renderRadar(${safeJsonValue(
|
||||
Json
|
||||
.obj(
|
||||
"radar" -> Json.obj(
|
||||
"labels" -> mostPlayed.map { case (key, _) =>
|
||||
PuzzleTheme(key).name.txt()
|
||||
},
|
||||
"datasets" -> Json.arr(
|
||||
// Json.obj(
|
||||
// "label" -> "# of puzzles",
|
||||
// "data" -> mostPlayed.map { case (_, results) =>
|
||||
// results.nb
|
||||
// }
|
||||
// ),
|
||||
Json.obj(
|
||||
"label" -> "Performance",
|
||||
"data" -> mostPlayed.map { case (_, results) =>
|
||||
results.performance
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)})""")
|
||||
)
|
||||
}
|
||||
) { dash =>
|
||||
frag(
|
||||
div(cls := s"${baseClass}__global")(
|
||||
metricsOf(days, PuzzleTheme.mix.key, dash.global)
|
||||
metricsOf(days, PuzzleTheme.mix.key, dash.global),
|
||||
canvas(cls := s"${baseClass}__radar")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// data: {
|
||||
// labels: ['Running', 'Swimming', 'Eating', 'Cycling'],
|
||||
// datasets: [{
|
||||
// data: [20, 10, 4, 2]
|
||||
// }]
|
||||
// }
|
||||
|
||||
def improvementAreas(user: User, dashOpt: Option[PuzzleDashboard], days: Int)(implicit ctx: Context) =
|
||||
dashboardLayout(
|
||||
user = user,
|
||||
|
@ -70,13 +112,15 @@ object dashboard {
|
|||
path: String,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
dashOpt: Option[PuzzleDashboard]
|
||||
dashOpt: Option[PuzzleDashboard],
|
||||
moreJs: Frag = emptyFrag
|
||||
)(
|
||||
body: PuzzleDashboard => Frag
|
||||
)(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
title = title,
|
||||
moreCss = cssTag("puzzle.dashboard")
|
||||
moreCss = cssTag("puzzle.dashboard"),
|
||||
moreJs = moreJs
|
||||
)(
|
||||
main(cls := "page-menu")(
|
||||
bits.pageMenu("dashboard"),
|
||||
|
|
|
@ -27,7 +27,7 @@ object history {
|
|||
div(cls := "puzzle-history")(
|
||||
div(cls := "infinite-scroll")(
|
||||
pager.currentPageResults map renderSession,
|
||||
pagerNext(pager, np => routes.Puzzle.history(np).url)
|
||||
pagerNext(pager, np => s"${routes.Puzzle.history(np).url}${!ctx.is(user) ?? s"&u=${user.id}"}")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -33,6 +33,8 @@ case class PuzzleDashboard(
|
|||
.takeRight(topThemesNb)
|
||||
(weaks, strong)
|
||||
}
|
||||
|
||||
def mostPlayed = byTheme.toList.sortBy(-_._2.nb).take(9)
|
||||
}
|
||||
|
||||
object PuzzleDashboard {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package lila.puzzle
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import org.joda.time.DateTime
|
||||
import reactivemongo.api.bson.BSONNull
|
||||
import reactivemongo.api.ReadPreference
|
||||
|
@ -7,22 +8,29 @@ import scala.concurrent.duration._
|
|||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.chaining._
|
||||
|
||||
import lila.common.config.MaxPerPage
|
||||
import lila.common.paginator.AdapterLike
|
||||
import lila.common.paginator.Paginator
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi
|
||||
import lila.user.User
|
||||
import lila.common.paginator.Paginator
|
||||
import lila.common.config.MaxPerPage
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
object PuzzleHistory {
|
||||
|
||||
val maxPerPage = MaxPerPage(100)
|
||||
|
||||
case class SessionRound(round: PuzzleRound, puzzle: Puzzle, theme: PuzzleTheme.Key)
|
||||
|
||||
case class PuzzleSession(
|
||||
theme: PuzzleTheme.Key,
|
||||
puzzles: NonEmptyList[SessionRound] // chronological order, oldest first
|
||||
)
|
||||
) {
|
||||
// val nb = puzzles.size
|
||||
// val firstWins = puzzles.toList.count(_.round.firstWin)
|
||||
// val fails = nb - firstWins
|
||||
// def puzzleRatingAvg = puzzles.toList.foldLeft(0)(_ + _.puzzle.glicko.intRating)
|
||||
// def performance = puzzleRatingAvg - 500 + math.round(1000 * (firstWins.toFloat / nb))
|
||||
}
|
||||
|
||||
final class HistoryAdapter(user: User, colls: PuzzleColls)(implicit ec: ExecutionContext)
|
||||
extends AdapterLike[PuzzleSession] {
|
||||
|
@ -60,13 +68,13 @@ object PuzzleHistory {
|
|||
rounds
|
||||
.foldLeft(List.empty[PuzzleSession]) {
|
||||
case (Nil, round) => List(PuzzleSession(round.theme, NonEmptyList(round, Nil)))
|
||||
case (last :: sessions, round) =>
|
||||
case (last :: sessions, r) =>
|
||||
if (
|
||||
last.puzzles.head.theme == round.theme &&
|
||||
last.puzzles.head.round.date.isAfter(round.round.date minusMinutes 15)
|
||||
last.puzzles.head.theme == r.theme &&
|
||||
r.round.date.isAfter(last.puzzles.head.round.date minusHours 1)
|
||||
)
|
||||
last.copy(puzzles = round :: last.puzzles) :: sessions
|
||||
else PuzzleSession(round.theme, NonEmptyList(round, Nil)) :: last :: sessions
|
||||
last.copy(puzzles = r :: last.puzzles) :: sessions
|
||||
else PuzzleSession(r.theme, NonEmptyList(r, Nil)) :: last :: sessions
|
||||
}
|
||||
.reverse
|
||||
}
|
||||
|
@ -82,7 +90,7 @@ final class PuzzleHistoryApi(
|
|||
Paginator[PuzzleSession](
|
||||
new HistoryAdapter(user, colls),
|
||||
currentPage = page,
|
||||
maxPerPage = MaxPerPage(50)
|
||||
maxPerPage = maxPerPage
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ case class PuzzleRound(
|
|||
win = win,
|
||||
fixedAt = fixedAt orElse win.option(DateTime.now)
|
||||
)
|
||||
|
||||
def firstWin = win && fixedAt.isEmpty
|
||||
}
|
||||
|
||||
object PuzzleRound {
|
||||
|
|
3
ui/build
3
ui/build
|
@ -19,6 +19,7 @@ apps3="site swiss msg chat cli challenge notify learn insight editor puzzle roun
|
|||
site_plugins="tvEmbed puzzleEmbed analyseEmbed user modUser clas coordinate captcha expandText team forum account coachShow coachForm challengePage checkout login passwordComplexity tourForm teamBattleForm gameSearch userComplete infiniteScroll flatpickr teamAdmin"
|
||||
round_plugins="nvui keyboardMove"
|
||||
analyse_plugins="nvui studyTopicForm"
|
||||
puzzle_plugins="dashboard"
|
||||
|
||||
if [ $mode == "upgrade" ]; then
|
||||
yarn upgrade --non-interactive
|
||||
|
@ -66,11 +67,13 @@ else
|
|||
parallel --gnu $P_OPTS build_plugin site ::: $site_plugins
|
||||
parallel --gnu $P_OPTS build_plugin round ::: $round_plugins
|
||||
parallel --gnu $P_OPTS build_plugin analyse ::: $analyse_plugins
|
||||
parallel --gnu $P_OPTS build_plugin analyse ::: $puzzle_plugins
|
||||
else # sequential execution
|
||||
echo "For faster builds, install GNU parallel."
|
||||
for app in $apps1 $apps2 $apps3; do (build $app); done
|
||||
for plugin in $site_plugins; do (build_plugin site $plugin); done
|
||||
for plugin in $round_plugins; do (build_plugin round $plugin); done
|
||||
for plugin in $analyse_plugins; do (build_plugin analyse $plugin); done
|
||||
for plugin in $puzzle_plugins; do (build_plugin puzzle $plugin); done
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -40,6 +40,12 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
&__radar {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
&__metrics {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
.puzzle-history {
|
||||
|
||||
&__session {
|
||||
margin-bottom: 5em;
|
||||
|
||||
&__title {
|
||||
@extend %flex-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
time {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 1ch;
|
||||
}
|
||||
}
|
||||
&__rounds {
|
||||
display: grid;
|
||||
|
@ -13,7 +20,7 @@
|
|||
}
|
||||
|
||||
&__round {
|
||||
padding: .3em;
|
||||
padding: .4em;
|
||||
color: $c-font;
|
||||
|
||||
&__meta {
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
"@types/lichess": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/chart.js": "^2.9.29",
|
||||
"ceval": "2.0.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"chessground": "^7.9.2",
|
||||
"chessops": "^0.8.1",
|
||||
"common": "2.0.0",
|
||||
|
@ -26,6 +28,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"dev": "rollup --config",
|
||||
"prod": "rollup --config --config-prod"
|
||||
"prod": "rollup --config --config-prod",
|
||||
"plugin-dev": "rollup --config --config-plugin",
|
||||
"plugin-prod": "rollup --config --config-prod --config-plugin"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,4 +6,9 @@ export default rollupProject({
|
|||
input: 'src/main.ts',
|
||||
output: 'puzzle',
|
||||
},
|
||||
dashboard: {
|
||||
name: 'LichessPuzzleDashboard',
|
||||
input: 'src/dashboard.ts',
|
||||
output: 'puzzle.dashboard',
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import Chart from 'chart.js';
|
||||
|
||||
export function renderRadar(data: any) {
|
||||
$('.puzzle-dashboard__radar').each(function() {
|
||||
const d = data.radar;
|
||||
d.datasets[0] = {
|
||||
...d.datasets[0],
|
||||
...{
|
||||
backgroundColor: 'rgba(189,130,35,.1)',
|
||||
borderColor: 'rgba(189,130,35,1)'
|
||||
}
|
||||
};
|
||||
|
||||
const chart = new Chart(this, {
|
||||
type: 'radar',
|
||||
data: d,
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
font: {
|
||||
size: 140
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
42
yarn.lock
42
yarn.lock
|
@ -123,6 +123,13 @@
|
|||
estree-walker "^1.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@types/chart.js@^2.9.29":
|
||||
version "2.9.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.29.tgz#73bf7f02387402943f29946012492f10bde7ed43"
|
||||
integrity sha512-WOZMitUU3gHDM0oQsCsVivX+oDsIki93szcTmmUPBm39cCvAELBjokjSDVOoA3xiIEbb+jp17z/3S2tIqruwOQ==
|
||||
dependencies:
|
||||
moment "^2.10.2"
|
||||
|
||||
"@types/defer-promise@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/defer-promise/-/defer-promise-1.0.0.tgz#9c5054b7ef63fd0f0497d17b0ea1a849f9c6323b"
|
||||
|
@ -719,6 +726,29 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chart.js@^2.9.4:
|
||||
version "2.9.4"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
|
||||
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
|
||||
dependencies:
|
||||
chartjs-color "^2.1.0"
|
||||
moment "^2.10.2"
|
||||
|
||||
chartjs-color-string@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
|
||||
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
|
||||
chartjs-color@^2.1.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
|
||||
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
|
||||
dependencies:
|
||||
chartjs-color-string "^0.6.0"
|
||||
color-convert "^1.9.3"
|
||||
|
||||
"chess.js@github:ornicar/chess.js#learn":
|
||||
version "0.10.2"
|
||||
resolved "https://codeload.github.com/ornicar/chess.js/tar.gz/ad0709c0b07773d9d0da8e4605a9a2a28f00d249"
|
||||
|
@ -846,7 +876,7 @@ collection-visit@^1.0.0:
|
|||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
color-convert@^1.9.0, color-convert@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
|
@ -858,6 +888,11 @@ color-name@1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
color-name@^1.0.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
color-support@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
|
||||
|
@ -3018,6 +3053,11 @@ mixin-deep@^1.2.0:
|
|||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
moment@^2.10.2:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
|
Loading…
Reference in New Issue