Merge branch 'master' of github.com:ornicar/lila

* 'master' of github.com:ornicar/lila:
  tweak relation caches
  show full list of friends
  remove timeline entries about people following me
  Revert "no longer send follow to friends timelines"
  no longer send follow to friends timelines
  ublog post image alt & credit
  analyse/embed/css: button cursor: pointer
  analyse/embed/css: Fix zh layout on mobile
  Prettier
  broadcast: Fix error URL
  ublog: Translate follow button properly
  css: Reduce page h2 line-height
  css: Make .button display: inline-block
  ublog/css: Prevent linebreak inside view count
  Fix zulip chat panic link
  Make shepherd close button visible
  Fix shepherd themes being swapped
pull/9850/head
Thibault Duplessis 2021-09-20 07:39:19 +02:00
commit 2fe175294e
33 changed files with 646 additions and 159 deletions

View File

@ -117,8 +117,9 @@ final class Relation(
OptionFuResult(env.user.repo named username) { user => OptionFuResult(env.user.repo named username) { user =>
RelatedPager(api.followingPaginatorAdapter(user.id), page) flatMap { pag => RelatedPager(api.followingPaginatorAdapter(user.id), page) flatMap { pag =>
negotiate( negotiate(
html = api countFollowers user.id map { nbFollowers => html = {
Ok(html.relation.bits.following(user, pag, nbFollowers)) if (ctx is user) Ok(html.relation.bits.friends(user, pag)).fuccess
else ctx.me.fold(notFound)(me => Redirect(routes.Relation.following(me.username)).fuccess)
}, },
api = _ => Ok(jsonRelatedPaginator(pag)).fuccess api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
) )
@ -129,18 +130,15 @@ final class Relation(
def followers(username: String, page: Int) = def followers(username: String, page: Int) =
Open { implicit ctx => Open { implicit ctx =>
Reasonable(page, 20) { negotiate(
OptionFuResult(env.user.repo named username) { user => html = notFound,
RelatedPager(api.followersPaginatorAdapter(user.id), page) flatMap { pag => api = _ =>
negotiate( Reasonable(page, 20) {
html = api countFollowing user.id map { nbFollowing => RelatedPager(api.followersPaginatorAdapter(UserModel normalize username), page) flatMap { pag =>
Ok(html.relation.bits.followers(user, pag, nbFollowing)) Ok(jsonRelatedPaginator(pag)).fuccess
}, }
api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
)
} }
} )
}
} }
def apiFollowing(name: String) = apiRelation(name, Direction.Following) def apiFollowing(name: String) = apiRelation(name, Direction.Following)

View File

@ -512,7 +512,7 @@ final class User(
} }
} }
.sequenceFu .sequenceFu
} yield html.user.opponents(me, relateds) } yield html.relation.bits.opponents(me, relateds)
} }
def perfStat(username: String, perfKey: String) = def perfStat(username: String, perfKey: String) =

View File

@ -1,44 +1,23 @@
package views.html.relation package views.html.relation
import controllers.routes
import play.api.mvc.Call import play.api.mvc.Call
import lila.api.Context import lila.api.Context
import lila.app.templating.Environment._ import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._ import lila.app.ui.ScalatagsTemplate._
import lila.common.paginator.Paginator import lila.common.paginator.Paginator
import lila.game.FavoriteOpponents
import lila.relation.Related import lila.relation.Related
import lila.user.User import lila.user.User
import controllers.routes
object bits { object bits {
def followers(u: User, pag: Paginator[Related], nbFollowing: Int)(implicit ctx: Context) = def friends(u: User, pag: Paginator[Related])(implicit ctx: Context) =
layout(s"${u.username}${trans.nbFollowers.pluralSameTxt(pag.nbResults)}")( layout(s"${u.username}${trans.friends.txt()}")(
div(cls := "box__top")( h1(
h1(userLink(u, withOnline = false)), a(href := routes.User.show(u.username), dataIcon := "", cls := "text"),
div(cls := "actions")( trans.friends()
trans.nbFollowers.pluralSame(pag.nbResults),
" ",
amp,
" ",
a(href := routes.Relation.following(u.username))(trans.nbFollowing.pluralSame(nbFollowing))
)
),
pagTable(pag, routes.Relation.followers(u.username))
)
def following(u: User, pag: Paginator[Related], nbFollowers: Int)(implicit ctx: Context) =
layout(s"${u.username}${trans.nbFollowing.pluralSameTxt(pag.nbResults)}")(
div(cls := "box__top")(
h1(userLink(u, withOnline = false)),
div(cls := "actions")(
trans.nbFollowing.pluralSame(pag.nbResults),
" ",
amp,
" ",
a(href := routes.Relation.followers(u.username))(trans.nbFollowers.pluralSame(nbFollowers))
)
), ),
pagTable(pag, routes.Relation.following(u.username)) pagTable(pag, routes.Relation.following(u.username))
) )
@ -54,6 +33,38 @@ object bits {
pagTable(pag, routes.Relation.blocks()) pagTable(pag, routes.Relation.blocks())
) )
def opponents(u: User, sugs: List[lila.relation.Related])(implicit ctx: Context) =
layout(s"${u.username}${trans.favoriteOpponents.txt()}")(
h1(
a(href := routes.User.show(u.username), dataIcon := "", cls := "text"),
trans.favoriteOpponents(),
" (",
trans.nbGames.pluralSame(FavoriteOpponents.gameLimit),
")"
),
table(cls := "slist")(
tbody(
if (sugs.nonEmpty) sugs.map { r =>
tr(
td(userLink(r.user)),
td(showBestPerf(r.user)),
td(
r.nbGames.filter(_ > 0).map { nbGames =>
a(href := s"${routes.User.games(u.username, "search")}?players.b=${r.user.username}")(
trans.nbGames.plural(nbGames, nbGames.localize)
)
}
),
td(
views.html.relation.actions(r.user.id, r.relation, followable = r.followable, blocked = false)
)
)
}
else tr(td(trans.none()))
)
)
)
def layout(title: String)(content: Modifier*)(implicit ctx: Context) = def layout(title: String)(content: Modifier*)(implicit ctx: Context) =
views.html.base.layout( views.html.base.layout(
title = title, title = title,
@ -71,7 +82,7 @@ object bits {
tr(cls := "paginated")( tr(cls := "paginated")(
td(userLink(r.user)), td(userLink(r.user)),
td(showBestPerf(r.user)), td(showBestPerf(r.user)),
td(trans.nbGames.pluralSame(r.user.count.game)), td(trans.nbGames.plural(r.user.count.game, r.user.count.game.localize)),
td(actions(r.user.id, relation = r.relation, followable = r.followable, blocked = false)) td(actions(r.user.id, relation = r.relation, followable = r.followable, blocked = false))
) )
}, },

View File

@ -26,7 +26,7 @@ object form {
) { ) {
main(cls := "page-menu page-small")( main(cls := "page-menu page-small")(
views.html.blog.bits.menu(none, "mine".some), views.html.blog.bits.menu(none, "mine".some),
div(cls := "page-menu__content box box-pad ublog-post-form")( div(cls := "page-menu__content box ublog-post-form")(
standardFlash(), standardFlash(),
h1(trans.ublog.newPost()), h1(trans.ublog.newPost()),
etiquette, etiquette,
@ -43,7 +43,7 @@ object form {
) { ) {
main(cls := "page-menu page-small")( main(cls := "page-menu page-small")(
views.html.blog.bits.menu(none, "mine".some), views.html.blog.bits.menu(none, "mine".some),
div(cls := "page-menu__content box box-pad ublog-post-form")( div(cls := "page-menu__content box ublog-post-form")(
standardFlash(), standardFlash(),
div(cls := "box__top")( div(cls := "box__top")(
h1( h1(
@ -108,32 +108,41 @@ object form {
) )
) )
def formImage(post: UblogPost) = postView.thumbnail(post, _.Small) def formImage(post: UblogPost) =
postView.thumbnail(post, _.Small)(cls := post.image.isDefined.option("user-image"))
private def inner(form: Form[UblogPostData], post: Either[User, UblogPost], captcha: Option[Captcha])( private def inner(form: Form[UblogPostData], post: Either[User, UblogPost], captcha: Option[Captcha])(
implicit ctx: Context implicit ctx: Context
) = ) =
postForm( postForm(
cls := "form3", cls := "form3 ublog-post-form__main",
action := post.fold(_ => routes.Ublog.create, p => routes.Ublog.update(p.id.value)) action := post.fold(_ => routes.Ublog.create, p => routes.Ublog.update(p.id.value))
)( )(
form3.globalError(form), form3.globalError(form),
post.isRight option form3.split( post.toOption.map { p =>
form3.checkbox( frag(
form("live"), form3.split(
trans.ublog.publishOnYourBlog(), form3.group(form("imageAlt"), trans.ublog.imageAlt(), half = true)(form3.input(_)),
help = trans.ublog.publishHelp().some, form3.group(form("imageCredit"), trans.ublog.imageCredit(), half = true)(form3.input(_))
half = true )(cls := s"ublog-post-form__image-text ${p.image.isDefined ?? "visible"}"),
), form3.split(
form3.group(form("language"), trans.language(), half = true) { field => form3.checkbox(
form3.select( form("live"),
field, trans.ublog.publishOnYourBlog(),
LangList.popularNoRegion.map { l => help = trans.ublog.publishHelp().some,
l.code -> l.toLocale.getDisplayLanguage half = true
),
form3.group(form("language"), trans.language(), half = true) { field =>
form3.select(
field,
LangList.popularNoRegion.map { l =>
l.code -> l.toLocale.getDisplayLanguage
}
)
} }
) )
} )
), },
form3.group(form("title"), trans.ublog.postTitle())(form3.input(_)(autofocus)), form3.group(form("title"), trans.ublog.postTitle())(form3.input(_)(autofocus)),
form3.group(form("intro"), trans.ublog.postIntro())(form3.input(_)(autofocus)), form3.group(form("intro"), trans.ublog.postIntro())(form3.input(_)(autofocus)),
form3.group( form3.group(

View File

@ -44,7 +44,12 @@ object post {
main(cls := "page-menu page-small")( main(cls := "page-menu page-small")(
views.html.blog.bits.menu(none, (if (ctx is user) "mine" else "community").some), views.html.blog.bits.menu(none, (if (ctx is user) "mine" else "community").some),
div(cls := "page-menu__content box box-pad ublog-post")( div(cls := "page-menu__content box box-pad ublog-post")(
post.image.isDefined option thumbnail(post, _.Large)(cls := "ublog-post__image"), post.image.map { image =>
frag(
thumbnail(post, _.Large)(cls := "ublog-post__image"),
image.credit.map { p(cls := "ublog-post__image-credit")(_) }
)
},
ctx.is(user) || isGranted(_.ModerateBlog) option standardFlash(), ctx.is(user) || isGranted(_.ModerateBlog) option standardFlash(),
h1(cls := "ublog-post__title")(post.title), h1(cls := "ublog-post__title")(post.title),
div(cls := "ublog-post__meta")( div(cls := "ublog-post__meta")(
@ -135,15 +140,15 @@ object post {
) )
)( )(
List( List(
("yes", trans.following, routes.Relation.unfollow _, ""), ("yes", trans.unfollowX, routes.Relation.unfollow _, ""),
("no", trans.follow, routes.Relation.follow _, "") ("no", trans.followX, routes.Relation.follow _, "")
).map { case (role, text, route, icon) => ).map { case (role, text, route, icon) =>
button( button(
cls := s"ublog-post__follow__$role button button-big", cls := s"ublog-post__follow__$role button button-big",
dataIcon := icon, dataIcon := icon,
dataRel := route(user.id) dataRel := route(user.id)
)( )(
span(cls := "button-label")(text.txt(), " ", user.titleUsername) span(cls := "button-label")(text(user.titleUsername))
) )
} }
) )
@ -191,12 +196,13 @@ object post {
img( img(
cls := "ublog-post-image", cls := "ublog-post-image",
widthA := size(UblogPost.thumbnail).width, widthA := size(UblogPost.thumbnail).width,
heightA := size(UblogPost.thumbnail).height heightA := size(UblogPost.thumbnail).height,
alt := post.image.flatMap(_.alt)
)(src := url(post, size)) )(src := url(post, size))
def url(post: UblogPost.BasePost, size: UblogPost.thumbnail.SizeSelector) = def url(post: UblogPost.BasePost, size: UblogPost.thumbnail.SizeSelector) =
post.image match { post.image match {
case Some(image) => UblogPost.thumbnail(picfitUrl, image, size) case Some(image) => UblogPost.thumbnail(picfitUrl, image.id, size)
case _ => assetUrl("images/user-blog-default.png") case _ => assetUrl("images/user-blog-default.png")
} }
} }

View File

@ -1,42 +0,0 @@
package views.html
package user
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.user.User
import lila.game.FavoriteOpponents
import controllers.routes
object opponents {
def apply(u: User, sugs: List[lila.relation.Related])(implicit ctx: Context) =
relation.bits.layout(s"${u.username}${trans.favoriteOpponents.txt()}")(
h1(
a(href := routes.User.show(u.username), dataIcon := "", cls := "text"),
trans.favoriteOpponents(),
" (",
trans.nbGames.pluralSame(FavoriteOpponents.gameLimit),
")"
),
table(cls := "slist")(
tbody(
if (sugs.nonEmpty) sugs.map { r =>
tr(
td(userLink(r.user)),
td(showBestPerf(r.user)),
td(
r.nbGames.filter(_ > 0).map { nbGames =>
a(href := s"${routes.User.games(u.username, "search")}?players.b=${r.user.username}")(
trans.nbGames.pluralSame(nbGames)
)
}
),
td(relation.actions(r.user.id, r.relation, followable = r.followable, blocked = false))
)
}
else tr(td(trans.none()))
)
)
)
}

View File

@ -48,9 +48,6 @@ object header {
), ),
div(cls := "user-show__social")( div(cls := "user-show__social")(
div(cls := "number-menu")( div(cls := "number-menu")(
a(cls := "nm-item", href := routes.Relation.followers(u.username))(
splitNumber(trans.nbFollowers.pluralSame(info.nbFollowers))
),
u.noBot option a( u.noBot option a(
href := routes.UserTournament.path(u.username, "recent"), href := routes.UserTournament.path(u.username, "recent"),
cls := "nm-item tournament_stats", cls := "nm-item tournament_stats",
@ -232,6 +229,8 @@ object header {
trans.profileCompletion(s"${profile.completionPercent}%") trans.profileCompletion(s"${profile.completionPercent}%")
), ),
br, br,
a(href := routes.Relation.following(u.username))(trans.friends()),
br,
a(href := routes.User.opponents)(trans.favoriteOpponents()) a(href := routes.User.opponents)(trans.favoriteOpponents())
), ),
u.playTime.map { playTime => u.playTime.map { playTime =>

View File

@ -0,0 +1,3 @@
db.ublog_post
.find({ image: { $exists: 1 } }, { image: 1 })
.forEach(p => db.ublog_post.update({ _id: p._id }, { $set: { image: { id: p.image } } }));

View File

@ -161,7 +161,7 @@ final class PersonalDataExport(
"title" -> post.title, "title" -> post.title,
"intro" -> post.intro, "intro" -> post.intro,
"body" -> post.markdown, "body" -> post.markdown,
"image" -> post.image.??(lila.ublog.UblogPost.thumbnail(picfitUrl, _, _.Large)), "image" -> post.image.??(i => lila.ublog.UblogPost.thumbnail(picfitUrl, i.id, _.Large)),
"topics" -> post.topics.map(_.value).mkString(", ") "topics" -> post.topics.map(_.value).mkString(", ")
).map { case (k, v) => ).map { case (k, v) =>
s"$k: $v" s"$k: $v"

View File

@ -277,6 +277,8 @@ val `favoriteOpponents` = new I18nKey("favoriteOpponents")
val `follow` = new I18nKey("follow") val `follow` = new I18nKey("follow")
val `following` = new I18nKey("following") val `following` = new I18nKey("following")
val `unfollow` = new I18nKey("unfollow") val `unfollow` = new I18nKey("unfollow")
val `followX` = new I18nKey("followX")
val `unfollowX` = new I18nKey("unfollowX")
val `block` = new I18nKey("block") val `block` = new I18nKey("block")
val `blocked` = new I18nKey("blocked") val `blocked` = new I18nKey("blocked")
val `unblock` = new I18nKey("unblock") val `unblock` = new I18nKey("unblock")
@ -2281,6 +2283,8 @@ val `noPostsInThisBlogYet` = new I18nKey("ublog:noPostsInThisBlogYet")
val `noDrafts` = new I18nKey("ublog:noDrafts") val `noDrafts` = new I18nKey("ublog:noDrafts")
val `latestBlogPosts` = new I18nKey("ublog:latestBlogPosts") val `latestBlogPosts` = new I18nKey("ublog:latestBlogPosts")
val `uploadAnImageForYourPost` = new I18nKey("ublog:uploadAnImageForYourPost") val `uploadAnImageForYourPost` = new I18nKey("ublog:uploadAnImageForYourPost")
val `imageAlt` = new I18nKey("ublog:imageAlt")
val `imageCredit` = new I18nKey("ublog:imageCredit")
val `publishedNbBlogPosts` = new I18nKey("ublog:publishedNbBlogPosts") val `publishedNbBlogPosts` = new I18nKey("ublog:publishedNbBlogPosts")
val `nbViews` = new I18nKey("ublog:nbViews") val `nbViews` = new I18nKey("ublog:nbViews")
val `viewAllNbPosts` = new I18nKey("ublog:viewAllNbPosts") val `viewAllNbPosts` = new I18nKey("ublog:viewAllNbPosts")

View File

@ -87,7 +87,7 @@ final class IrcApi(
def chatPanic(mod: Holder, v: Boolean): Funit = def chatPanic(mod: Holder, v: Boolean): Funit =
zulip(_.mod.log, "chat panic")( zulip(_.mod.log, "chat panic")(
s":stop: ${markdown.modLink(mod.user)} ${if (v) "enabled" else "disabled"} ${markdown.lichessLink("mod/chat-panic", " Chat Panic")}" s":stop: ${markdown.modLink(mod.user)} ${if (v) "enabled" else "disabled"} ${markdown.lichessLink("/mod/chat-panic", " Chat Panic")}"
) )
def garbageCollector(msg: String): Funit = def garbageCollector(msg: String): Funit =

View File

@ -80,9 +80,8 @@ final class RelationApi(
def fetchAreFriends(u1: ID, u2: ID): Fu[Boolean] = def fetchAreFriends(u1: ID, u2: ID): Fu[Boolean] =
fetchFollows(u1, u2) >>& fetchFollows(u2, u1) fetchFollows(u1, u2) >>& fetchFollows(u2, u1)
private val countFollowingCache = cacheApi[ID, Int](8192, "relation.count.following") { private val countFollowingCache = cacheApi[ID, Int](8_192, "relation.count.following") {
_.expireAfterAccess(10 minutes) _.maximumSize(8_192)
.maximumSize(32768)
.buildAsyncFuture { userId => .buildAsyncFuture { userId =>
coll.countSel($doc("u1" -> userId, "r" -> Follow)) coll.countSel($doc("u1" -> userId, "r" -> Follow))
} }
@ -92,9 +91,8 @@ final class RelationApi(
def reachedMaxFollowing(userId: ID): Fu[Boolean] = countFollowingCache get userId map (config.maxFollow <=) def reachedMaxFollowing(userId: ID): Fu[Boolean] = countFollowingCache get userId map (config.maxFollow <=)
private val countFollowersCache = cacheApi[ID, Int](131_072, "relation.count.followers") { private val countFollowersCache = cacheApi[ID, Int](32_768, "relation.count.followers") {
_.expireAfterAccess(10 minutes) _.maximumSize(32_768)
.maximumSize(131_072)
.buildAsyncFuture { userId => .buildAsyncFuture { userId =>
coll.secondaryPreferred.countSel($doc("u2" -> userId, "r" -> Follow)) coll.secondaryPreferred.countSel($doc("u2" -> userId, "r" -> Follow))
} }
@ -147,7 +145,7 @@ final class RelationApi(
repo.follow(u1, u2) >> limitFollow(u1) >>- { repo.follow(u1, u2) >> limitFollow(u1) >>- {
countFollowersCache.update(u2, 1 +) countFollowersCache.update(u2, 1 +)
countFollowingCache.update(u1, prev => (prev + 1) atMost config.maxFollow.value) countFollowingCache.update(u1, prev => (prev + 1) atMost config.maxFollow.value)
timeline ! Propagate(FollowUser(u1, u2)).toFriendsOf(u1).toUsers(List(u2)) timeline ! Propagate(FollowUser(u1, u2)).toFriendsOf(u1)
Bus.publish(lila.hub.actorApi.relation.Follow(u1, u2), "relation") Bus.publish(lila.hub.actorApi.relation.Follow(u1, u2), "relation")
lila.mon.relation.follow.increment().unit lila.mon.relation.follow.increment().unit
} }

View File

@ -84,7 +84,7 @@ object JsonView {
"log" -> s.log.events "log" -> s.log.events
) ++ ) ++
s.upstream.?? { s.upstream.?? {
case RelayRound.Sync.UpstreamUrl(url) => Json.obj("url" -> url) case url: RelayRound.Sync.UpstreamUrl => Json.obj("url" -> url.withRound.url)
case RelayRound.Sync.UpstreamIds(ids) => Json.obj("ids" -> ids) case RelayRound.Sync.UpstreamIds(ids) => Json.obj("ids" -> ids)
} }
} }

View File

@ -110,10 +110,10 @@ final class UblogApi(
def uploadImage(user: User, post: UblogPost, picture: PicfitApi.FilePart): Fu[UblogPost] = def uploadImage(user: User, post: UblogPost, picture: PicfitApi.FilePart): Fu[UblogPost] =
for { for {
image <- picfitApi pic <- picfitApi.uploadFile(imageRel(post), picture, userId = user.id)
.uploadFile(imageRel(post), picture, userId = user.id) image = post.image.fold(UblogImage(pic.id))(_.copy(id = pic.id))
_ <- colls.post.update.one($id(post.id), $set("image" -> image.id)) _ <- colls.post.updateField($id(post.id), "image", image)
} yield post.copy(image = image.id.some) } yield post.copy(image = image.some)
def deleteImage(post: UblogPost): Fu[UblogPost] = def deleteImage(post: UblogPost): Fu[UblogPost] =
picfitApi.deleteByRel(imageRel(post)) >> picfitApi.deleteByRel(imageRel(post)) >>

View File

@ -25,6 +25,7 @@ private object UblogBsonHandlers {
.afterRead(_.filter(t => UblogTopic.exists(t.value))) .afterRead(_.filter(t => UblogTopic.exists(t.value)))
implicit val langBsonHandler = stringAnyValHandler[Lang](_.code, Lang.apply) implicit val langBsonHandler = stringAnyValHandler[Lang](_.code, Lang.apply)
implicit val recordedBSONHandler = Macros.handler[Recorded] implicit val recordedBSONHandler = Macros.handler[Recorded]
implicit val imageBSONHandler = Macros.handler[UblogImage]
implicit val likesBSONHandler = intAnyValHandler[Likes](_.value, Likes) implicit val likesBSONHandler = intAnyValHandler[Likes](_.value, Likes)
implicit val viewsBSONHandler = intAnyValHandler[Views](_.value, Views) implicit val viewsBSONHandler = intAnyValHandler[Views](_.value, Views)
implicit val rankBSONHandler = dateIsoHandler[Rank](Iso[DateTime, Rank](Rank, _.value)) implicit val rankBSONHandler = dateIsoHandler[Rank](Iso[DateTime, Rank](Rank, _.value))

View File

@ -17,14 +17,16 @@ final class UblogForm(markup: UblogMarkup, val captcher: lila.hub.actors.Captche
private val base = private val base =
mapping( mapping(
"title" -> cleanNonEmptyText(minLength = 3, maxLength = 80), "title" -> cleanNonEmptyText(minLength = 3, maxLength = 80),
"intro" -> cleanNonEmptyText(minLength = 0, maxLength = 1_000), "intro" -> cleanNonEmptyText(minLength = 0, maxLength = 1_000),
"markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).verifying(markdownImage.constraint), "markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).verifying(markdownImage.constraint),
"language" -> optional(stringIn(LangList.popularNoRegion.map(_.code).toSet)), "imageAlt" -> optional(cleanNonEmptyText(minLength = 3, maxLength = 200)),
"topics" -> optional(text), "imageCredit" -> optional(cleanNonEmptyText(minLength = 3, maxLength = 200)),
"live" -> boolean, "language" -> optional(stringIn(LangList.popularNoRegion.map(_.code).toSet)),
"gameId" -> text, "topics" -> optional(text),
"move" -> text "live" -> boolean,
"gameId" -> text,
"move" -> text
)(UblogPostData.apply)(UblogPostData.unapply) )(UblogPostData.apply)(UblogPostData.unapply)
val create = Form( val create = Form(
@ -37,6 +39,8 @@ final class UblogForm(markup: UblogMarkup, val captcher: lila.hub.actors.Captche
title = post.title, title = post.title,
intro = post.intro, intro = post.intro,
markdown = post.markdown, markdown = post.markdown,
imageAlt = post.image.flatMap(_.alt),
imageCredit = post.image.flatMap(_.credit),
language = post.language.code.some, language = post.language.code.some,
topics = post.topics.map(_.value).mkString(", ").some, topics = post.topics.map(_.value).mkString(", ").some,
live = post.live, live = post.live,
@ -52,6 +56,8 @@ object UblogForm {
title: String, title: String,
intro: String, intro: String,
markdown: String, markdown: String,
imageAlt: Option[String],
imageCredit: Option[String],
language: Option[String], language: Option[String],
topics: Option[String], topics: Option[String],
live: Boolean, live: Boolean,
@ -84,6 +90,9 @@ object UblogForm {
title = title, title = title,
intro = intro, intro = intro,
markdown = markdown, markdown = markdown,
image = prev.image.map { i =>
i.copy(alt = imageAlt, credit = imageCredit)
},
language = LangList.removeRegion(realLanguage | prev.language), language = LangList.removeRegion(realLanguage | prev.language),
topics = topics ?? UblogTopic.fromStrList, topics = topics ?? UblogTopic.fromStrList,
live = live, live = live,

View File

@ -13,7 +13,7 @@ case class UblogPost(
intro: String, intro: String,
markdown: String, markdown: String,
language: Lang, language: Lang,
image: Option[PicfitImage.Id], image: Option[UblogImage],
topics: List[UblogTopic], topics: List[UblogTopic],
live: Boolean, live: Boolean,
created: UblogPost.Recorded, created: UblogPost.Recorded,
@ -28,6 +28,8 @@ case class UblogPost(
def indexable = live && topics.exists(t => UblogTopic.chessExists(t.value)) def indexable = live && topics.exists(t => UblogTopic.chessExists(t.value))
} }
case class UblogImage(id: PicfitImage.Id, alt: Option[String] = None, credit: Option[String] = None)
object UblogPost { object UblogPost {
case class Id(value: String) extends AnyVal with StringValue case class Id(value: String) extends AnyVal with StringValue
@ -50,7 +52,7 @@ object UblogPost {
val blog: UblogBlog.Id val blog: UblogBlog.Id
val title: String val title: String
val intro: String val intro: String
val image: Option[PicfitImage.Id] val image: Option[UblogImage]
val created: Recorded val created: Recorded
val lived: Option[Recorded] val lived: Option[Recorded]
def id = _id def id = _id
@ -62,7 +64,7 @@ object UblogPost {
blog: UblogBlog.Id, blog: UblogBlog.Id,
title: String, title: String,
intro: String, intro: String,
image: Option[PicfitImage.Id], image: Option[UblogImage],
created: Recorded, created: Recorded,
lived: Option[Recorded] lived: Option[Recorded]
) extends BasePost ) extends BasePost

View File

@ -2,7 +2,7 @@ lichess.ratingHistoryChart = function (data, { singlePerfName, perfIndex }) {
var oneDay = 86400000; var oneDay = 86400000;
function smoothDates(data) { function smoothDates(data) {
if (!data.length) return []; if (!data.length) return [];
// If last rating wasn't today, add to the array // If last rating wasn't today, add to the array
var today = new Date().setUTCHours(0, 0, 0, 0); var today = new Date().setUTCHours(0, 0, 0, 0);
var lastRating = data[data.length - 1]; var lastRating = data[data.length - 1];

View File

@ -1,6 +1,6 @@
function loadShepherd(f) { function loadShepherd(f) {
var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'default' : 'dark'); var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'dark' : 'default');
lichess.loadCss('vendor/shepherd/dist/css/' + theme + '.css'); lichess.loadCss('vendor/' + theme + '.css');
lichess.loadScript('vendor/shepherd/dist/js/tether.js', { noVersion: true }).then(function () { lichess.loadScript('vendor/shepherd/dist/js/tether.js', { noVersion: true }).then(function () {
lichess.loadScript('vendor/shepherd/dist/js/shepherd.min.js', { noVersion: true }).then(function () { lichess.loadScript('vendor/shepherd/dist/js/shepherd.min.js', { noVersion: true }).then(function () {
f(theme); f(theme);

View File

@ -1,7 +1,7 @@
function loadShepherd(f) { function loadShepherd(f) {
if (typeof Shepherd === 'undefined' || Shepherd.activeTour === null) { if (typeof Shepherd === 'undefined' || Shepherd.activeTour === null) {
var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'default' : 'dark'); var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'dark' : 'default');
lichess.loadCss('vendor/shepherd/dist/css/' + theme + '.css'); lichess.loadCss('vendor/' + theme + '.css');
lichess.loadScript('vendor/shepherd/dist/js/tether.js', { noVersion: true }).then(function () { lichess.loadScript('vendor/shepherd/dist/js/tether.js', { noVersion: true }).then(function () {
lichess.loadScript('vendor/shepherd/dist/js/shepherd.min.js', { noVersion: true }).then(function () { lichess.loadScript('vendor/shepherd/dist/js/shepherd.min.js', { noVersion: true }).then(function () {
f(theme); f(theme);

View File

@ -0,0 +1,213 @@
/* https://raw.githubusercontent.com/shipshapecode/shepherd/e3ed0fcbf5c31137ece409d3ad6fea4e264ca1cc/dist/css/shepherd-theme-dark.css */
.shepherd-element, .shepherd-element:after, .shepherd-element:before, .shepherd-element *, .shepherd-element *:after, .shepherd-element *:before {
box-sizing: border-box; }
.shepherd-element {
position: absolute;
display: none; }
.shepherd-element.shepherd-open {
display: block; }
.shepherd-element.shepherd-theme-dark {
max-width: 100%;
max-height: 100%; }
.shepherd-element.shepherd-theme-dark .shepherd-content {
border-radius: 5px;
position: relative;
font-family: inherit;
background: #232323;
color: #eee;
padding: 1em;
font-size: 1.1em;
line-height: 1.5em; }
.shepherd-element.shepherd-theme-dark .shepherd-content:before {
content: "";
display: block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-width: 16px;
border-style: solid;
pointer-events: none; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-center .shepherd-content {
margin-bottom: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-center .shepherd-content:before {
top: 100%;
left: 50%;
margin-left: -16px;
border-top-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-center .shepherd-content {
margin-top: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-center .shepherd-content:before {
bottom: 100%;
left: 50%;
margin-left: -16px;
border-bottom-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-right.shepherd-element-attached-middle .shepherd-content {
margin-right: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-right.shepherd-element-attached-middle .shepherd-content:before {
left: 100%;
top: 50%;
margin-top: -16px;
border-left-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-left.shepherd-element-attached-middle .shepherd-content {
margin-left: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-left.shepherd-element-attached-middle .shepherd-content:before {
right: 100%;
top: 50%;
margin-top: -16px;
border-right-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-bottom .shepherd-content {
margin-top: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-bottom .shepherd-content:before {
bottom: 100%;
left: 16px;
border-bottom-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-bottom .shepherd-content {
margin-top: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-bottom .shepherd-content:before {
bottom: 100%;
right: 16px;
border-bottom-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-top .shepherd-content {
margin-bottom: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-top .shepherd-content:before {
top: 100%;
left: 16px;
border-top-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-top .shepherd-content {
margin-bottom: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-top .shepherd-content:before {
top: 100%;
right: 16px;
border-top-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content {
margin-right: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content:before {
top: 16px;
left: 100%;
border-left-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content {
margin-left: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content:before {
top: 16px;
right: 100%;
border-right-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content {
margin-right: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content:before {
bottom: 16px;
left: 100%;
border-left-color: #232323; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content {
margin-left: 16px; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content:before {
bottom: 16px;
right: 100%;
border-right-color: #232323; }
.shepherd-element.shepherd-theme-dark {
z-index: 9999;
max-width: 24em;
font-size: 1em; }
.shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-center.shepherd-has-title .shepherd-content:before, .shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-bottom.shepherd-has-title .shepherd-content:before, .shepherd-element.shepherd-theme-dark.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-bottom.shepherd-has-title .shepherd-content:before {
border-bottom-color: #303030; }
.shepherd-element.shepherd-theme-dark.shepherd-has-title .shepherd-content header {
background: #303030;
padding: 1em; }
.shepherd-element.shepherd-theme-dark.shepherd-has-title .shepherd-content header a.shepherd-cancel-link {
padding: 0;
margin-bottom: 0; }
.shepherd-element.shepherd-theme-dark.shepherd-has-cancel-link .shepherd-content header h3 {
float: left; }
.shepherd-element.shepherd-theme-dark .shepherd-content {
box-shadow: 0 0 1em rgba(0, 0, 0, 0.2);
padding: 0; }
.shepherd-element.shepherd-theme-dark .shepherd-content * {
font-size: inherit; }
.shepherd-element.shepherd-theme-dark .shepherd-content header {
*zoom: 1;
border-radius: 5px 5px 0 0; }
.shepherd-element.shepherd-theme-dark .shepherd-content header:after {
content: "";
display: table;
clear: both; }
.shepherd-element.shepherd-theme-dark .shepherd-content header h3 {
margin: 0;
line-height: 1;
font-weight: normal; }
.shepherd-element.shepherd-theme-dark .shepherd-content header a.shepherd-cancel-link {
float: right;
text-decoration: none;
font-size: 1.25em;
line-height: .8em;
font-weight: normal;
color: white;
opacity: 0.6;
position: relative;
top: .1em;
padding: .8em;
margin-bottom: -.8em; }
.shepherd-element.shepherd-theme-dark .shepherd-content header a.shepherd-cancel-link:hover {
opacity: 1; }
.shepherd-element.shepherd-theme-dark .shepherd-content .shepherd-text {
padding: 1em; }
.shepherd-element.shepherd-theme-dark .shepherd-content .shepherd-text p {
margin: 0 0 .5em 0;
line-height: 1.3em; }
.shepherd-element.shepherd-theme-dark .shepherd-content .shepherd-text p:last-child {
margin-bottom: 0; }
.shepherd-element.shepherd-theme-dark .shepherd-content footer {
padding: 0 1em 1em; }
.shepherd-element.shepherd-theme-dark .shepherd-content footer .shepherd-buttons {
text-align: right;
list-style: none;
padding: 0;
margin: 0; }
.shepherd-element.shepherd-theme-dark .shepherd-content footer .shepherd-buttons li {
display: inline;
padding: 0;
margin: 0; }
.shepherd-element.shepherd-theme-dark .shepherd-content footer .shepherd-buttons li .shepherd-button {
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
*zoom: 1;
*display: inline;
border-radius: 3px;
cursor: pointer;
border: 0;
margin: 0 .5em 0 0;
font-family: inherit;
text-transform: uppercase;
letter-spacing: .1em;
font-size: .8em;
line-height: 1em;
padding: .75em 2em;
background: #3288e6;
color: #fff; }
.shepherd-element.shepherd-theme-dark .shepherd-content footer .shepherd-buttons li .shepherd-button.shepherd-button-secondary {
background: #eee;
color: #888; }
.shepherd-element.shepherd-theme-dark .shepherd-content footer .shepherd-buttons li:last-child .shepherd-button {
margin-right: 0; }
.shepherd-start-tour-button.shepherd-theme-dark {
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
*zoom: 1;
*display: inline;
border-radius: 3px;
cursor: pointer;
border: 0;
margin: 0 .5em 0 0;
font-family: inherit;
text-transform: uppercase;
letter-spacing: .1em;
font-size: .8em;
line-height: 1em;
padding: .75em 2em;
background: #3288e6;
color: #fff; }

View File

@ -0,0 +1,213 @@
/* https://raw.githubusercontent.com/shipshapecode/shepherd/e3ed0fcbf5c31137ece409d3ad6fea4e264ca1cc/dist/css/shepherd-theme-default.css */
.shepherd-element, .shepherd-element:after, .shepherd-element:before, .shepherd-element *, .shepherd-element *:after, .shepherd-element *:before {
box-sizing: border-box; }
.shepherd-element {
position: absolute;
display: none; }
.shepherd-element.shepherd-open {
display: block; }
.shepherd-element.shepherd-theme-default {
max-width: 100%;
max-height: 100%; }
.shepherd-element.shepherd-theme-default .shepherd-content {
border-radius: 5px;
position: relative;
font-family: inherit;
background: #f6f6f6;
color: #444;
padding: 1em;
font-size: 1.1em;
line-height: 1.5em; }
.shepherd-element.shepherd-theme-default .shepherd-content:before {
content: "";
display: block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-width: 16px;
border-style: solid;
pointer-events: none; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-center .shepherd-content {
margin-bottom: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-center .shepherd-content:before {
top: 100%;
left: 50%;
margin-left: -16px;
border-top-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-center .shepherd-content {
margin-top: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-center .shepherd-content:before {
bottom: 100%;
left: 50%;
margin-left: -16px;
border-bottom-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-right.shepherd-element-attached-middle .shepherd-content {
margin-right: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-right.shepherd-element-attached-middle .shepherd-content:before {
left: 100%;
top: 50%;
margin-top: -16px;
border-left-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-left.shepherd-element-attached-middle .shepherd-content {
margin-left: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-left.shepherd-element-attached-middle .shepherd-content:before {
right: 100%;
top: 50%;
margin-top: -16px;
border-right-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-bottom .shepherd-content {
margin-top: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-bottom .shepherd-content:before {
bottom: 100%;
left: 16px;
border-bottom-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-bottom .shepherd-content {
margin-top: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-bottom .shepherd-content:before {
bottom: 100%;
right: 16px;
border-bottom-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-top .shepherd-content {
margin-bottom: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-top .shepherd-content:before {
top: 100%;
left: 16px;
border-top-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-top .shepherd-content {
margin-bottom: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-top .shepherd-content:before {
top: 100%;
right: 16px;
border-top-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content {
margin-right: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content:before {
top: 16px;
left: 100%;
border-left-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content {
margin-left: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content:before {
top: 16px;
right: 100%;
border-right-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content {
margin-right: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-right.shepherd-target-attached-left .shepherd-content:before {
bottom: 16px;
left: 100%;
border-left-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content {
margin-left: 16px; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-bottom.shepherd-element-attached-left.shepherd-target-attached-right .shepherd-content:before {
bottom: 16px;
right: 100%;
border-right-color: #f6f6f6; }
.shepherd-element.shepherd-theme-default {
z-index: 9999;
max-width: 24em;
font-size: 1em; }
.shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-center.shepherd-has-title .shepherd-content:before, .shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-right.shepherd-target-attached-bottom.shepherd-has-title .shepherd-content:before, .shepherd-element.shepherd-theme-default.shepherd-element-attached-top.shepherd-element-attached-left.shepherd-target-attached-bottom.shepherd-has-title .shepherd-content:before {
border-bottom-color: #e6e6e6; }
.shepherd-element.shepherd-theme-default.shepherd-has-title .shepherd-content header {
background: #e6e6e6;
padding: 1em; }
.shepherd-element.shepherd-theme-default.shepherd-has-title .shepherd-content header a.shepherd-cancel-link {
padding: 0;
margin-bottom: 0; }
.shepherd-element.shepherd-theme-default.shepherd-has-cancel-link .shepherd-content header h3 {
float: left; }
.shepherd-element.shepherd-theme-default .shepherd-content {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.17);
padding: 0; }
.shepherd-element.shepherd-theme-default .shepherd-content * {
font-size: inherit; }
.shepherd-element.shepherd-theme-default .shepherd-content header {
*zoom: 1;
border-radius: 5px 5px 0 0; }
.shepherd-element.shepherd-theme-default .shepherd-content header:after {
content: "";
display: table;
clear: both; }
.shepherd-element.shepherd-theme-default .shepherd-content header h3 {
margin: 0;
line-height: 1;
font-weight: normal; }
.shepherd-element.shepherd-theme-default .shepherd-content header a.shepherd-cancel-link {
float: right;
text-decoration: none;
font-size: 1.25em;
line-height: .8em;
font-weight: normal;
color: black;
opacity: 0.6;
position: relative;
top: .1em;
padding: .8em;
margin-bottom: -.8em; }
.shepherd-element.shepherd-theme-default .shepherd-content header a.shepherd-cancel-link:hover {
opacity: 1; }
.shepherd-element.shepherd-theme-default .shepherd-content .shepherd-text {
padding: 1em; }
.shepherd-element.shepherd-theme-default .shepherd-content .shepherd-text p {
margin: 0 0 .5em 0;
line-height: 1.3em; }
.shepherd-element.shepherd-theme-default .shepherd-content .shepherd-text p:last-child {
margin-bottom: 0; }
.shepherd-element.shepherd-theme-default .shepherd-content footer {
padding: 0 1em 1em; }
.shepherd-element.shepherd-theme-default .shepherd-content footer .shepherd-buttons {
text-align: right;
list-style: none;
padding: 0;
margin: 0; }
.shepherd-element.shepherd-theme-default .shepherd-content footer .shepherd-buttons li {
display: inline;
padding: 0;
margin: 0; }
.shepherd-element.shepherd-theme-default .shepherd-content footer .shepherd-buttons li .shepherd-button {
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
*zoom: 1;
*display: inline;
border-radius: 3px;
cursor: pointer;
border: 0;
margin: 0 .5em 0 0;
font-family: inherit;
text-transform: uppercase;
letter-spacing: .1em;
font-size: .8em;
line-height: 1em;
padding: .75em 2em;
background: #3288e6;
color: #fff; }
.shepherd-element.shepherd-theme-default .shepherd-content footer .shepherd-buttons li .shepherd-button.shepherd-button-secondary {
background: #eee;
color: #888; }
.shepherd-element.shepherd-theme-default .shepherd-content footer .shepherd-buttons li:last-child .shepherd-button {
margin-right: 0; }
.shepherd-start-tour-button.shepherd-theme-default {
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
*zoom: 1;
*display: inline;
border-radius: 3px;
cursor: pointer;
border: 0;
margin: 0 .5em 0 0;
font-family: inherit;
text-transform: uppercase;
letter-spacing: .1em;
font-size: .8em;
line-height: 1em;
padding: .75em 2em;
background: #3288e6;
color: #fff; }

View File

@ -382,6 +382,8 @@ computer analysis, game chat and shareable URL.</string>
<string name="follow">Follow</string> <string name="follow">Follow</string>
<string name="following">Following</string> <string name="following">Following</string>
<string name="unfollow">Unfollow</string> <string name="unfollow">Unfollow</string>
<string name="followX">Follow %s</string>
<string name="unfollowX">Unfollow %s</string>
<string name="block">Block</string> <string name="block">Block</string>
<string name="blocked">Blocked</string> <string name="blocked">Blocked</string>
<string name="unblock">Unblock</string> <string name="unblock">Unblock</string>

View File

@ -31,4 +31,6 @@
<item quantity="other">View all %s posts</item> <item quantity="other">View all %s posts</item>
</plurals> </plurals>
<string name="uploadAnImageForYourPost">Upload an image for your post</string> <string name="uploadAnImageForYourPost">Upload an image for your post</string>
<string name="imageAlt">Image alternative text</string>
<string name="imageCredit">Image credit</string>
</resources> </resources>

View File

@ -42,3 +42,7 @@ cg-board {
padding: 2em 4em; padding: 2em 4em;
} }
} }
button {
cursor: pointer;
}

View File

@ -33,8 +33,10 @@
&.gamebook-play { &.gamebook-play {
@media (min-width: 400px) and (min-aspect-ratio: 1/1) { @media (min-width: 400px) and (min-aspect-ratio: 1/1) {
grid-template-columns: minmax(200px, calc(100vh - 2.5rem)) minmax(200px, 1fr); grid-template-columns: minmax(200px, calc(100vh - 2.5rem)) minmax(200px, 1fr);
grid-template-rows: auto 2.5rem; grid-template-rows: auto 3rem;
grid-template-areas: 'board tools' 'board controls'; grid-template-areas:
'board tools'
'board controls';
} }
} }
} }
@ -55,11 +57,21 @@
} }
.analyse.variant-crazyhouse { .analyse.variant-crazyhouse {
grid-template-rows: 60px auto 2.5rem 60px; grid-template-areas:
'pocket-top'
'board'
'pocket-bot'
'tools'
'controls';
grid-template-rows: auto 100vw auto 1fr 3rem;
height: calc(100vh - 2.5rem);
body.supports-max-content & { @media (min-width: 400px) and (min-aspect-ratio: 1/1) {
grid-template-rows: max-content auto 2.5rem max-content; grid-template-rows: auto 1fr 3rem auto;
grid-template-areas:
'board pocket-top'
'board tools'
'board controls'
'board pocket-bot';
} }
grid-template-areas: 'board pocket-top' 'board tools' 'board controls' 'board pocket-bot';
} }

View File

@ -9,6 +9,7 @@
text-align: center; text-align: center;
user-select: none; user-select: none;
white-space: nowrap; white-space: nowrap;
display: inline-block;
@include transition; @include transition;

View File

@ -3,9 +3,10 @@
font-size: 1.1em; font-size: 1.1em;
h2 { h2 {
margin: 1.3em 0 0.5em 0; margin: 1.8em 0 0.5em 0;
border-bottom: 1px solid $c-brag; border-bottom: 1px solid $c-brag;
line-height: 2.5em; line-height: 1.5em;
padding-bottom: 0.5em;
} }
p, p,
@ -32,7 +33,8 @@
@extend %roboto; @extend %roboto;
font-size: 1.5em; font-size: 1.5em;
line-height: 2.5em; line-height: 1.5em;
margin-top: 1.3em;
} }
h4 { h4 {

View File

@ -1,6 +1,35 @@
@import 'flash'; @import 'flash';
.ublog-post-form { .ublog-post-form {
padding-bottom: 5vh;
&__main .form-split,
&__main > .form-group,
&__main .form-actions,
&__delete .form-actions,
&__image {
padding-left: var(--box-padding);
padding-right: var(--box-padding);
}
&__image {
background: $c-bg-zebra;
padding-top: 5vh;
}
&__image-text {
background: $c-bg-zebra;
margin-bottom: 5vh;
border-bottom: $border;
font-size: 0.9em;
.form-group {
display: none;
}
&.visible {
height: auto;
.form-group {
display: block;
}
}
}
.ublog-post-image { .ublog-post-image {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
@ -13,10 +42,8 @@
} }
&__image { &__image {
.form-group { .form-group {
text-align: center;
@extend %flex-column; @extend %flex-column;
justify-content: center; justify-content: center;
align-items: center;
} }
img { img {
@extend %box-neat; @extend %box-neat;

View File

@ -52,9 +52,10 @@
} }
h2 { h2 {
margin-top: 1.3em; margin-top: 1.8em;
border-bottom: 1px solid $c-brag; border-bottom: 1px solid $c-brag;
line-height: 2.5em; line-height: 1.5em;
padding-bottom: 0.5em;
} }
h3 { h3 {

View File

@ -77,10 +77,18 @@
} }
} }
} }
&__views {
white-space: nowrap;
}
&__image { &__image {
@extend %box-neat-force; @extend %box-neat-force;
width: 100%; width: 100%;
height: auto; height: auto;
&-credit {
font-size: 0.9em;
color: $c-font-dim;
text-align: right;
}
} }
&__intro { &__intro {
@extend %break-word; @extend %break-word;

View File

@ -3,8 +3,6 @@ import exportLichessGlobals from './site.lichess.globals';
exportLichessGlobals(); exportLichessGlobals();
export default function (opts: any) { export default function (opts: any) {
document.body.classList.toggle('supports-max-content', !!window.chrome);
window.LichessAnalyse.start({ window.LichessAnalyse.start({
...opts, ...opts,
socketSend: () => {}, socketSend: () => {},

View File

@ -28,11 +28,16 @@ const setupTopics = (el: HTMLTextAreaElement) =>
}); });
const setupImage = (form: HTMLFormElement) => { const setupImage = (form: HTMLFormElement) => {
const showText = () =>
$('.ublog-post-form__image-text').toggleClass('visible', $('.ublog-post-image').hasClass('user-image'));
const submit = () => { const submit = () => {
const replace = (html: string) => $(form).find('.ublog-post-image').replaceWith(html); const replace = (html: string) => $(form).find('.ublog-post-image').replaceWith(html);
const wrap = (html: string) => '<div class="ublog-post-image">' + html + '</div>'; const wrap = (html: string) => '<div class="ublog-post-image">' + html + '</div>';
xhr.formToXhr(form).then( xhr.formToXhr(form).then(
html => replace(html), html => {
replace(html);
showText();
},
err => replace(wrap(`<bad>${err}</bad>`)) err => replace(wrap(`<bad>${err}</bad>`))
); );
replace(wrap(spinner)); replace(wrap(spinner));
@ -40,6 +45,7 @@ const setupImage = (form: HTMLFormElement) => {
}; };
$(form).on('submit', submit); $(form).on('submit', submit);
$(form).find('input[name="image"]').on('change', submit); $(form).find('input[name="image"]').on('change', submit);
showText();
}; };
const setupMarkdownEditor = (el: HTMLTextAreaElement) => { const setupMarkdownEditor = (el: HTMLTextAreaElement) => {