ublog topics page WIP
parent
4282b6bcf3
commit
1802221a93
|
@ -226,10 +226,18 @@ final class Ublog(env: Env) extends LilaController(env) {
|
|||
}
|
||||
}
|
||||
|
||||
def topics = Open { implicit ctx =>
|
||||
NotForKids {
|
||||
env.ublog.topic.withPosts map { topics =>
|
||||
Ok(html.ublog.index.topics(topics))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def topic(str: String, page: Int) = Open { implicit ctx =>
|
||||
NotForKids {
|
||||
Reasonable(page, 15) {
|
||||
UblogPost.Topic.fromUrl(str) ?? { top =>
|
||||
lila.ublog.UblogTopic.fromUrl(str) ?? { top =>
|
||||
env.ublog.paginator.liveByTopic(top, page) map { posts =>
|
||||
Ok(html.ublog.index.topic(top, posts))
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ object bits {
|
|||
def menu(year: Option[Int], active: Option[String])(implicit ctx: Context) =
|
||||
st.nav(cls := "page-menu__menu subnav")(
|
||||
a(cls := active.has("community").option("active"), href := routes.Ublog.community())("Community blogs"),
|
||||
a(cls := active.has("topics").option("active"), href := routes.Ublog.topics)("Blog topics"),
|
||||
a(cls := active.has("friends").option("active"), href := routes.Ublog.friends())("Friends blogs"),
|
||||
a(cls := active.has("liked").option("active"), href := routes.Ublog.liked())("Liked blog posts"),
|
||||
ctx.me map { me =>
|
||||
|
|
|
@ -9,7 +9,7 @@ import lila.app.ui.ScalatagsTemplate._
|
|||
import lila.common.Captcha
|
||||
import lila.i18n.LangList
|
||||
import lila.ublog.UblogForm.UblogPostData
|
||||
import lila.ublog.UblogPost
|
||||
import lila.ublog.{ UblogPost, UblogTopic }
|
||||
import lila.user.User
|
||||
|
||||
object form {
|
||||
|
@ -117,7 +117,7 @@ object form {
|
|||
)
|
||||
},
|
||||
form3.group(form("topics"), frag("Select the topics your post is about"))(
|
||||
form3.textarea(_)(dataRel := UblogPost.Topic.all.mkString(","))
|
||||
form3.textarea(_)(dataRel := UblogTopic.all.mkString(","))
|
||||
),
|
||||
captcha.fold(views.html.base.captcha.hiddenEmpty(form)) { c =>
|
||||
views.html.base.captcha(form, c)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package views.html.ublog
|
||||
|
||||
import controllers.routes
|
||||
import play.api.mvc.Call
|
||||
|
||||
import lila.api.Context
|
||||
import lila.app.templating.Environment._
|
||||
import lila.app.ui.ScalatagsTemplate._
|
||||
import lila.common.paginator.Paginator
|
||||
import lila.ublog.UblogPost
|
||||
import lila.ublog.{ UblogPost, UblogTopic }
|
||||
import lila.user.User
|
||||
import play.api.mvc.Call
|
||||
|
||||
object index {
|
||||
|
||||
|
@ -59,10 +59,10 @@ object index {
|
|||
onEmpty = "Nothing to show. Like some posts!"
|
||||
)
|
||||
|
||||
def topic(top: UblogPost.Topic, posts: Paginator[UblogPost.PreviewPost])(implicit ctx: Context) = list(
|
||||
def topic(top: UblogTopic, posts: Paginator[UblogPost.PreviewPost])(implicit ctx: Context) = list(
|
||||
title = s"Blog posts about $top",
|
||||
posts = posts,
|
||||
menuItem = top.value,
|
||||
menuItem = "topics",
|
||||
route = p => routes.Ublog.topic(top.value, p),
|
||||
onEmpty = "Nothing to show."
|
||||
)
|
||||
|
@ -75,6 +75,34 @@ object index {
|
|||
onEmpty = "Nothing to show."
|
||||
)
|
||||
|
||||
def topics(tops: List[UblogTopic.WithPosts])(implicit ctx: Context) =
|
||||
views.html.base.layout(
|
||||
moreCss = cssTag("ublog"),
|
||||
title = "All blog topics"
|
||||
) {
|
||||
main(cls := "page-menu")(
|
||||
views.html.blog.bits.menu(none, "topics".some),
|
||||
div(cls := "page-menu__content box box-pad ublog-index")(
|
||||
div(cls := "box__top")(h1("All blog topics")),
|
||||
div(cls := "ublog-topics")(
|
||||
tops.map { case UblogTopic.WithPosts(topic, posts, nb) =>
|
||||
st.section(cls := "ublog-topics__topic")(
|
||||
h2(
|
||||
a(href := routes.Ublog.topic(topic.url))(
|
||||
strong(topic.value),
|
||||
span(cls := "ublog-topics__topic__nb")(trans.ublog.viewAllNbPosts(nb), " »")
|
||||
)
|
||||
),
|
||||
div(cls := "ublog-topics__topic__posts ublog-post-cards")(
|
||||
posts map { postView.miniCard(_, showAuthor = true) }
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private def list(
|
||||
title: String,
|
||||
posts: Paginator[UblogPost.PreviewPost],
|
||||
|
|
|
@ -123,6 +123,18 @@ object post {
|
|||
)
|
||||
)
|
||||
|
||||
def miniCard(
|
||||
post: UblogPost.BasePost,
|
||||
makeUrl: UblogPost.BasePost => Call = urlOfPost,
|
||||
showAuthor: Boolean = false
|
||||
)(implicit ctx: Context) =
|
||||
a(cls := "ublog-post-card ublog-post-card--mini", href := makeUrl(post))(
|
||||
thumbnail(post, _.Small)(cls := "ublog-post-card__image"),
|
||||
h3(cls := "ublog-post-card__title")(post.title),
|
||||
post.lived map { live => semanticDate(live.at)(ctx.lang)(cls := "ublog-post-card__over-image") },
|
||||
showAuthor option userIdSpanMini(post.created.by)(ctx.lang)(cls := "ublog-post-card__over-image")
|
||||
)
|
||||
|
||||
def urlOfPost(post: UblogPost.BasePost) = post.blog match {
|
||||
case UblogBlog.Id.User(userId) =>
|
||||
routes.Ublog.post(usernameOrId(userId), post.slug, post.id.value)
|
||||
|
|
|
@ -97,6 +97,7 @@ GET /blog controllers.Blog.index(page: Int ?= 1)
|
|||
GET /blog/all controllers.Blog.all
|
||||
GET /blog/$year<2\d{3}> controllers.Blog.year(year: Int)
|
||||
GET /blog/discuss/:id controllers.Blog.discuss(id: String)
|
||||
GET /blog/topic controllers.Ublog.topics
|
||||
GET /blog/topic/:topic controllers.Ublog.topic(topic: String, page: Int ?= 1)
|
||||
GET /blog/:id/:slug controllers.Blog.show(id: String, slug: String, ref: Option[String] ?= None)
|
||||
GET /blog.atom controllers.Blog.atom
|
||||
|
|
|
@ -2275,6 +2275,7 @@ val `noDrafts` = new I18nKey("ublog:noDrafts")
|
|||
val `latestBlogPosts` = new I18nKey("ublog:latestBlogPosts")
|
||||
val `publishedNbBlogPosts` = new I18nKey("ublog:publishedNbBlogPosts")
|
||||
val `nbViews` = new I18nKey("ublog:nbViews")
|
||||
val `viewAllNbPosts` = new I18nKey("ublog:viewAllNbPosts")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -110,10 +110,9 @@ final class PlayerRepo(coll: Coll)(implicit ec: scala.concurrent.ExecutionContex
|
|||
teamId: TeamID
|
||||
): Fu[TeamBattle.TeamInfo] = {
|
||||
coll
|
||||
.aggregateWith[Bdoc]() { framework =>
|
||||
.aggregateOne() { framework =>
|
||||
import framework._
|
||||
List(
|
||||
Match(selectTour(tourId) ++ $doc("t" -> teamId)),
|
||||
Match(selectTour(tourId) ++ $doc("t" -> teamId)) -> List(
|
||||
Sort(Descending("m")),
|
||||
Facet(
|
||||
List(
|
||||
|
@ -130,20 +129,19 @@ final class PlayerRepo(coll: Coll)(implicit ec: scala.concurrent.ExecutionContex
|
|||
)
|
||||
)
|
||||
}
|
||||
.headOption
|
||||
.map {
|
||||
_.flatMap { doc =>
|
||||
for {
|
||||
aggs <- doc.getAsOpt[List[Bdoc]]("agg")
|
||||
agg <- aggs.headOption
|
||||
nbPlayers <- agg.int("nb")
|
||||
rating = agg.double("rating").??(math.round)
|
||||
perf = agg.double("perf").??(math.round)
|
||||
score = agg.double("score").??(math.round)
|
||||
topPlayers <- doc.getAsOpt[List[Player]]("topPlayers")
|
||||
} yield TeamBattle.TeamInfo(teamId, nbPlayers, rating.toInt, perf.toInt, score.toInt, topPlayers)
|
||||
} | TeamBattle.TeamInfo(teamId, 0, 0, 0, 0, Nil)
|
||||
.map { docO =>
|
||||
for {
|
||||
doc <- docO
|
||||
aggs <- doc.getAsOpt[List[Bdoc]]("agg")
|
||||
agg <- aggs.headOption
|
||||
nbPlayers <- agg.int("nb")
|
||||
rating = agg.double("rating").??(math.round)
|
||||
perf = agg.double("perf").??(math.round)
|
||||
score = agg.double("score").??(math.round)
|
||||
topPlayers <- doc.getAsOpt[List[Player]]("topPlayers")
|
||||
} yield TeamBattle.TeamInfo(teamId, nbPlayers, rating.toInt, perf.toInt, score.toInt, topPlayers)
|
||||
}
|
||||
.dmap(_ | TeamBattle.TeamInfo(teamId, 0, 0, 0, 0, Nil))
|
||||
}
|
||||
|
||||
def bestTeamPlayers(tourId: Tournament.ID, teamId: TeamID, nb: Int): Fu[List[Player]] =
|
||||
|
|
|
@ -23,6 +23,8 @@ final class Env(
|
|||
|
||||
private val colls = new UblogColls(db(CollName("ublog_blog")), db(CollName("ublog_post")))
|
||||
|
||||
val topic = wire[UblogTopicApi]
|
||||
|
||||
val rank = wire[UblogRank]
|
||||
|
||||
val api = wire[UblogApi]
|
||||
|
|
|
@ -20,9 +20,9 @@ private object UblogBsonHandlers {
|
|||
implicit val blogBSONHandler = Macros.handler[UblogBlog]
|
||||
|
||||
implicit val postIdBSONHandler = stringAnyValHandler[UblogPost.Id](_.value, UblogPost.Id)
|
||||
implicit val topicBsonHandler = stringAnyValHandler[UblogPost.Topic](_.value, UblogPost.Topic.apply)
|
||||
implicit val topicsBsonHandler = implicitly[BSONReader[List[UblogPost.Topic]]]
|
||||
.afterRead(_.filter(t => UblogPost.Topic.exists(t.value)))
|
||||
implicit val topicBsonHandler = stringAnyValHandler[UblogTopic](_.value, UblogTopic.apply)
|
||||
implicit val topicsBsonHandler = implicitly[BSONReader[List[UblogTopic]]]
|
||||
.afterRead(_.filter(t => UblogTopic.exists(t.value)))
|
||||
implicit val langBsonHandler = stringAnyValHandler[Lang](_.code, Lang.apply)
|
||||
implicit val recordedBSONHandler = Macros.handler[Recorded]
|
||||
implicit val likesBSONHandler = intAnyValHandler[Likes](_.value, Likes)
|
||||
|
|
|
@ -69,7 +69,7 @@ object UblogForm {
|
|||
intro = intro,
|
||||
markdown = markdown,
|
||||
language = LangList.removeRegion(realLanguage.orElse(user.realLang) | defaultLang),
|
||||
topics = topics ?? UblogPost.Topic.fromStrList,
|
||||
topics = topics ?? UblogTopic.fromStrList,
|
||||
image = none,
|
||||
live = false,
|
||||
created = UblogPost.Recorded(user.id, DateTime.now),
|
||||
|
@ -85,7 +85,7 @@ object UblogForm {
|
|||
intro = intro,
|
||||
markdown = markdown,
|
||||
language = LangList.removeRegion(realLanguage | prev.language),
|
||||
topics = topics ?? UblogPost.Topic.fromStrList,
|
||||
topics = topics ?? UblogTopic.fromStrList,
|
||||
live = live,
|
||||
updated = UblogPost.Recorded(user.id, DateTime.now).some,
|
||||
lived = prev.lived orElse live.option(UblogPost.Recorded(user.id, DateTime.now))
|
||||
|
|
|
@ -65,7 +65,7 @@ final class UblogPaginator(
|
|||
maxPerPage = maxPerPage
|
||||
)
|
||||
|
||||
def liveByTopic(topic: UblogPost.Topic, page: Int): Fu[Paginator[PreviewPost]] =
|
||||
def liveByTopic(topic: UblogTopic, page: Int): Fu[Paginator[PreviewPost]] =
|
||||
Paginator(
|
||||
adapter = new Adapter[PreviewPost](
|
||||
collection = colls.post,
|
||||
|
|
|
@ -14,7 +14,7 @@ case class UblogPost(
|
|||
markdown: String,
|
||||
language: Lang,
|
||||
image: Option[PicfitImage.Id],
|
||||
topics: List[UblogPost.Topic],
|
||||
topics: List[UblogTopic],
|
||||
live: Boolean,
|
||||
created: UblogPost.Recorded,
|
||||
updated: Option[UblogPost.Recorded],
|
||||
|
@ -25,7 +25,7 @@ case class UblogPost(
|
|||
|
||||
def isBy(u: User) = created.by == u.id
|
||||
|
||||
def indexable = live && topics.exists(t => UblogPost.Topic.chessExists(t.value))
|
||||
def indexable = live && topics.exists(t => UblogTopic.chessExists(t.value))
|
||||
}
|
||||
|
||||
object UblogPost {
|
||||
|
@ -34,46 +34,6 @@ object UblogPost {
|
|||
|
||||
case class Recorded(by: User.ID, at: DateTime)
|
||||
|
||||
case class Topic(value: String) extends StringValue {
|
||||
val url = value.replace(" ", "_")
|
||||
}
|
||||
|
||||
object Topic {
|
||||
val chess = List(
|
||||
"Chess",
|
||||
"Analysis",
|
||||
"Puzzle",
|
||||
"Opening",
|
||||
"Endgame",
|
||||
"Tactics",
|
||||
"Strategy",
|
||||
"Chess engine",
|
||||
"Chess bot",
|
||||
"Chess Personalities",
|
||||
"Over the board",
|
||||
"Tournament",
|
||||
"Chess960",
|
||||
"Crazyhouse",
|
||||
"Chess960",
|
||||
"King of the Hill",
|
||||
"Three-check",
|
||||
"Antichess",
|
||||
"Atomic",
|
||||
"Horde",
|
||||
"Racing Kings"
|
||||
)
|
||||
val all = chess ::: List(
|
||||
"Software Development",
|
||||
"Lichess",
|
||||
"Off topic"
|
||||
)
|
||||
val exists = all.toSet
|
||||
val chessExists = chess.toSet
|
||||
def get(str: String) = exists(str) option Topic(str)
|
||||
def fromStrList(str: String) = str.split(',').toList.flatMap(get).distinct
|
||||
def fromUrl(str: String) = get(str.replace("_", " "))
|
||||
}
|
||||
|
||||
case class Likes(value: Int) extends AnyVal
|
||||
case class Views(value: Int) extends AnyVal
|
||||
case class Rank(value: DateTime) extends AnyVal
|
||||
|
|
|
@ -38,7 +38,7 @@ final class UblogRank(colls: UblogColls)(implicit ec: ExecutionContext) {
|
|||
for {
|
||||
doc <- docOption
|
||||
likes <- doc.getAsOpt[UblogPost.Likes]("likes")
|
||||
topics <- doc.getAsOpt[List[UblogPost.Topic]]("topics")
|
||||
topics <- doc.getAsOpt[List[UblogTopic]]("topics")
|
||||
liveAt <- doc.getAsOpt[DateTime]("at")
|
||||
tier <- doc int "tier"
|
||||
} yield (topics, likes, liveAt, tier)
|
||||
|
@ -68,7 +68,7 @@ final class UblogRank(colls: UblogColls)(implicit ec: ExecutionContext) {
|
|||
lila.common.Future.applySequentially(docs) { doc =>
|
||||
(
|
||||
doc.string("_id"),
|
||||
doc.getAsOpt[List[UblogPost.Topic]]("topics"),
|
||||
doc.getAsOpt[List[UblogTopic]]("topics"),
|
||||
doc.getAsOpt[UblogPost.Likes]("likes"),
|
||||
doc.getAsOpt[UblogPost.Recorded]("lived")
|
||||
).tupled ?? { case (id, topics, likes, lived) =>
|
||||
|
@ -85,7 +85,7 @@ final class UblogRank(colls: UblogColls)(implicit ec: ExecutionContext) {
|
|||
}
|
||||
|
||||
private def computeRank(
|
||||
topics: List[UblogPost.Topic],
|
||||
topics: List[UblogTopic],
|
||||
likes: UblogPost.Likes,
|
||||
liveAt: DateTime,
|
||||
tier: UblogBlog.Tier
|
||||
|
@ -94,7 +94,7 @@ final class UblogRank(colls: UblogColls)(implicit ec: ExecutionContext) {
|
|||
val likeHours =
|
||||
if (likes.value < 1) 0
|
||||
else (5 * math.log(likes.value) + 1).toInt.atMost(likes.value) * 12
|
||||
val topicsMultiplier = topics.count(t => UblogPost.Topic.chessExists(t.value)) match {
|
||||
val topicsMultiplier = topics.count(t => UblogTopic.chessExists(t.value)) match {
|
||||
case 0 => 0.5
|
||||
case 1 => 1
|
||||
case _ => 1.5
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package lila.ublog
|
||||
|
||||
import reactivemongo.api.bson.BSONNull
|
||||
import reactivemongo.api.ReadPreference
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi
|
||||
|
||||
case class UblogTopic(value: String) extends StringValue {
|
||||
val url = value.replace(" ", "_")
|
||||
}
|
||||
|
||||
object UblogTopic {
|
||||
val chess = List(
|
||||
"Chess",
|
||||
"Analysis",
|
||||
"Puzzle",
|
||||
"Opening",
|
||||
"Endgame",
|
||||
"Tactics",
|
||||
"Strategy",
|
||||
"Chess engine",
|
||||
"Chess bot",
|
||||
"Chess Personalities",
|
||||
"Over the board",
|
||||
"Tournament",
|
||||
"Chess960",
|
||||
"Crazyhouse",
|
||||
"Chess960",
|
||||
"King of the Hill",
|
||||
"Three-check",
|
||||
"Antichess",
|
||||
"Atomic",
|
||||
"Horde",
|
||||
"Racing Kings"
|
||||
)
|
||||
val all = chess ::: List(
|
||||
"Software Development",
|
||||
"Lichess",
|
||||
"Off topic"
|
||||
)
|
||||
val exists = all.toSet
|
||||
val chessExists = chess.toSet
|
||||
def get(str: String) = exists(str) option UblogTopic(str)
|
||||
def fromStrList(str: String) = str.split(',').toList.flatMap(get).distinct
|
||||
def fromUrl(str: String) = get(str.replace("_", " "))
|
||||
|
||||
case class WithPosts(topic: UblogTopic, posts: List[UblogPost.PreviewPost], nb: Int)
|
||||
}
|
||||
|
||||
final class UblogTopicApi(colls: UblogColls, cacheApi: CacheApi)(implicit ec: ExecutionContext) {
|
||||
|
||||
import UblogBsonHandlers._
|
||||
|
||||
private val withPostsCache =
|
||||
cacheApi.unit[List[UblogTopic.WithPosts]](_.refreshAfterWrite(30 seconds).buildAsyncFuture { _ =>
|
||||
colls.post
|
||||
.aggregateList(UblogTopic.all.size, ReadPreference.secondaryPreferred) { framework =>
|
||||
import framework._
|
||||
Facet(
|
||||
UblogTopic.all.map { topic =>
|
||||
topic -> List(
|
||||
Match($doc("live" -> true, "topics" -> topic)),
|
||||
Project(previewPostProjection),
|
||||
Sort(Descending("rank")),
|
||||
Group(BSONNull)("nb" -> SumAll, "posts" -> PushField("$ROOT")),
|
||||
Project($doc("_id" -> false, "nb" -> true, "posts" -> $doc("$slice" -> $arr("$posts", 3))))
|
||||
)
|
||||
}
|
||||
) -> List(
|
||||
Project($doc("all" -> $doc("$objectToArray" -> "$$ROOT"))),
|
||||
UnwindField("all"),
|
||||
ReplaceRootField("all"),
|
||||
Unwind("v"),
|
||||
Project($doc("k" -> true, "nb" -> "$v.nb", "posts" -> "$v.posts"))
|
||||
)
|
||||
}
|
||||
.map { docs =>
|
||||
for {
|
||||
doc <- docs
|
||||
t <- doc string "k"
|
||||
topic <- UblogTopic.get(t)
|
||||
nb <- doc int "nb"
|
||||
posts <- doc.getAsOpt[List[UblogPost.PreviewPost]]("posts")
|
||||
} yield UblogTopic.WithPosts(topic, posts, nb)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
def withPosts: Fu[List[UblogTopic.WithPosts]] = withPostsCache.get {}
|
||||
}
|
|
@ -26,4 +26,8 @@
|
|||
<string name="noPostsInThisBlogYet">No posts in this blog, yet.</string>
|
||||
<string name="noDrafts">No drafts to show.</string>
|
||||
<string name="latestBlogPosts">Latest blog posts</string>
|
||||
<plurals name="viewAllNbPosts">
|
||||
<item quantity="one">View one post</item>
|
||||
<item quantity="other">View all %s posts</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
transition: 0.6s all ease-in-out;
|
||||
transition: 0.6s transform ease-in-out;
|
||||
overflow: hidden;
|
||||
transform-origin: bottom;
|
||||
.ublog-post-card:hover & {
|
||||
|
@ -69,5 +69,24 @@
|
|||
margin-top: 2vmin;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&--mini {
|
||||
font-size: 0.9em;
|
||||
line-height: 0;
|
||||
> * {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.ublog-post-card {
|
||||
&__title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 2vmin;
|
||||
background: $c-bg-zebra;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,4 +116,28 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-topics {
|
||||
.ublog-post-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
|
||||
grid-gap: 3vmin;
|
||||
}
|
||||
&__topic {
|
||||
margin-bottom: 3em;
|
||||
h2 {
|
||||
margin-bottom: 1rem;
|
||||
a {
|
||||
@extend %flex-between;
|
||||
}
|
||||
strong {
|
||||
letter-spacing: 0.5ch;
|
||||
font-weight: normal;
|
||||
}
|
||||
span {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue