lookup puzzles generated from games of a player - closes #8025

prod-hotfix
Thibault Duplessis 2021-01-28 22:30:35 +01:00
parent cb011dc5b6
commit b22e323149
10 changed files with 183 additions and 5 deletions

View File

@ -96,6 +96,16 @@ final class Puzzle(
}
}
def ofPlayer(name: Option[String], page: Int) =
Open { implicit ctx =>
val fixed = name.map(_.trim).filter(_.nonEmpty)
fixed.??(env.user.repo.named) orElse fuccess(ctx.me) flatMap { user =>
user.?? { env.puzzle.api.puzzle.of(_, page) dmap some } map { puzzles =>
Ok(views.html.puzzle.ofPlayer(~fixed, user, puzzles))
}
}
}
private def onComplete[A](form: Form[RoundData])(id: Puz.Id, theme: PuzzleTheme, mobileBc: Boolean)(implicit
ctx: BodyContext[A]
) = {

View File

@ -51,6 +51,9 @@ object bits {
),
a(cls := active.active("history"), href := routes.Puzzle.history(1))(
trans.puzzle.history()
),
a(cls := active.active("player"), href := routes.Puzzle.ofPlayer())(
"From my games"
)
)

View File

@ -0,0 +1,75 @@
package views
package html.puzzle
import controllers.routes
import play.api.i18n.Lang
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.paginator.Paginator
import lila.puzzle.Puzzle
import lila.puzzle.PuzzleTheme
import lila.user.User
object ofPlayer {
def apply(query: String, user: Option[User], puzzles: Option[Paginator[Puzzle]])(implicit ctx: Context) =
views.html.base.layout(
title = user.fold("Lookup puzzles from a player's games")(u => s"Puzzles from ${u.username}' games"),
moreCss = cssTag("puzzle.page"),
moreJs = infiniteScrollTag
)(
main(cls := "page-menu")(
bits.pageMenu("player"),
div(cls := "page-menu__content puzzle-of-player box box-pad")(
form(
action := routes.Puzzle.ofPlayer(),
method := "get",
cls := "form3 puzzle-of-player__form complete-parent"
)(
st.input(
name := "name",
value := query,
cls := "form-control user-autocomplete",
placeholder := "Lichess username",
autocomplete := "off",
dataTag := "span",
autofocus
),
submitButton(cls := "button")("Search puzzles")
),
div(cls := "puzzle-of-player__results")(
(user, puzzles) match {
case (Some(u), Some(pager)) =>
frag(
p(strong(pager.nbResults), " puzzles found in ", userLink(u), " games."),
div(cls := "puzzle-of-player__pager infinite-scroll")(
pager.currentPageResults.map { puzzle =>
div(cls := "puzzle-of-player__puzzle")(
views.html.board.bits.mini(
fen = puzzle.fenAfterInitialMove,
color = puzzle.color,
lastMove = puzzle.line.head.uci
)(
a(
cls := s"puzzle-of-player__puzzle__board",
href := routes.Puzzle.show(puzzle.id.value)
)
),
span(cls := "puzzle-of-player__puzzle__meta")(
span(cls := "puzzle-of-player__puzzle__id", s"#${puzzle.id}"),
span(cls := "puzzle-of-player__puzzle__rating", puzzle.glicko.intRating)
)
)
},
pagerNext(pager, np => s"${routes.Puzzle.ofPlayer(u.username.some, np).url}")
)
)
case (_, _) => emptyFrag
}
)
)
)
)
}

View File

@ -43,7 +43,14 @@ object theme {
span(pt.theme.description())
)
)
}
},
cat.key == "puzzle:origin" option
a(cls := "puzzle-themes__link", href := routes.Puzzle.ofPlayer())(
span(
h3("Player games"),
span("Lookup puzzles generated from your games, or from another player's games.")
)
)
)
)
},

View File

@ -74,8 +74,9 @@ object storm {
def dashboard(user: User, history: Paginator[StormDay], high: StormHigh)(implicit ctx: Context) =
views.html.base.layout(
title = s"${user.username} Puzzle Storm",
moreCss = frag(cssTag("storm.dashboard")),
title = s"${user.username} Puzzle Storm"
moreJs = infiniteScrollTag
)(
main(cls := "storm-dashboard page-small")(
div(cls := "storm-dashboard__high box box-pad")(
@ -106,7 +107,7 @@ object storm {
th(trans.storm.runs())
)
),
tbody(
tbody(cls := "infinite-scroll")(
history.currentPageResults.map { day =>
tr(
td(showDate(day._id.day.toDate)),

View File

@ -85,6 +85,7 @@ 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/of-player controllers.Puzzle.ofPlayer(name: Option[String] ?= None, page: Int ?= 1)
GET /training/dashboard/$days<\d+> controllers.Puzzle.dashboard(days: Int, path: String = "home")
GET /training/dashboard/$days<\d+>/:path controllers.Puzzle.dashboard(days: Int, path: String)
GET /training/replay/$days<\d+>/:theme controllers.Puzzle.replay(days: Int, theme: String)

View File

@ -4,8 +4,11 @@ import cats.implicits._
import org.joda.time.DateTime
import scala.concurrent.duration._
import lila.common.paginator.Paginator
import lila.common.config.MaxPerPage
import lila.db.AsyncColl
import lila.db.dsl._
import lila.db.paginator.Adapter
import lila.memo.CacheApi
import lila.user.{ User, UserRepo }
@ -25,6 +28,20 @@ final class PuzzleApi(
def delete(id: Puzzle.Id): Funit =
colls.puzzle(_.delete.one($id(id.value))).void
def of(user: User, page: Int): Fu[Paginator[Puzzle]] =
colls.puzzle { coll =>
Paginator(
adapter = new Adapter[Puzzle](
collection = coll,
selector = $doc("users" -> user.id),
projection = none,
sort = $sort asc "glicko.r"
),
page,
MaxPerPage(30)
)
}
}
object round {

View File

@ -1,7 +1,7 @@
.puzzle-themes {
h2 {
@extend %box-padding-horiz, %roboto;
margin-top: 1em;
}
@ -9,9 +9,12 @@
display: grid;
margin-left: var(--box-padding);
grid-template-columns: repeat(auto-fill, minmax(50ch, 1fr));
@include breakpoint($mq-not-xx-small) {
grid-template-columns: repeat(auto-fill, minmax(40ch, 1fr));
}
&.puzzle-recommended {
display: block;
}
@ -19,19 +22,23 @@
&__link {
@extend %box-radius;
display: flex;
padding: 1.5em 1em 1.5em 0;
&::before {
@extend %data-icon;
content: '-';
color: $c-font-dimmer;
flex: 0 0 1.6em;
text-align: center;
font-size: 4.5em;
}
&:hover {
background: mix($c-bg-box, $c-link, 90%);
&::before {
color: $c-primary;
}
@ -41,9 +48,11 @@
&::before {
display: none;
}
padding-left: 1em;
}
> span {
flex: 1 1 100%;
margin: 0;
@ -55,8 +64,10 @@
display: block;
line-height: 1em;
margin: .1em 0 .25em 0;
em {
@extend %roboto;
color: $c-font-dimmer;
font-size: .8em;
margin-left: .7ch;
@ -66,19 +77,24 @@
.puzzle-recommended & {
@extend %box-neat;
font-size: 1.2em;
background: mix($c-bg-box, $c-good, 80%);
color: $c-good;
margin: 2em 4em;
&::before {
color: $c-good;
}
> span {
color: $c-font;
}
&:hover {
background: mix($c-bg-box, $c-good, 74%);
}
@include breakpoint($mq-not-small) {
margin: 2em var(--box-padding) 2em 0;
}
@ -87,6 +103,54 @@
&__db {
@extend %box-padding;
text-align: center;
}
}
.puzzle-of-player {
&__form {
input {
margin-right: 1em;
width: 30ch;
display: inline-block;
}
}
&__pager {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12em, 1fr));
align-content: start;
}
&__results {
p {
font-size: 1.5em;
margin: 2em 0;
}
}
&__puzzle {
padding: .4em;
&__meta {
@extend %flex-between;
font-size: .9em;
padding: 0 .3em;
}
&__rating {
}
&__id {
opacity: 0;
@include transition;
}
&:hover .puzzle-of-player__puzzle__id {
opacity: 1;
}
}
}

View File

@ -1,3 +1,4 @@
@import "../../../common/css/plugin";
@import "../../../common/css/form/form3";
@import "../page";

View File

@ -1,5 +1,4 @@
@import "../../../common/css/plugin";
@import "../../../common/css/vendor/chessground/coords";
@import "../../../common/css/layout/uniboard";
@import "../../../common/css/component/board-resize";
@import "../../../common/css/component/bar-glider";