diff --git a/app/controllers/Relation.scala b/app/controllers/Relation.scala
index 13f35e56b0..04f69ca2e7 100644
--- a/app/controllers/Relation.scala
+++ b/app/controllers/Relation.scala
@@ -117,8 +117,9 @@ final class Relation(
OptionFuResult(env.user.repo named username) { user =>
RelatedPager(api.followingPaginatorAdapter(user.id), page) flatMap { pag =>
negotiate(
- html = api countFollowers user.id map { nbFollowers =>
- Ok(html.relation.bits.following(user, pag, nbFollowers))
+ html = {
+ 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
)
@@ -129,18 +130,15 @@ final class Relation(
def followers(username: String, page: Int) =
Open { implicit ctx =>
- Reasonable(page, 20) {
- OptionFuResult(env.user.repo named username) { user =>
- RelatedPager(api.followersPaginatorAdapter(user.id), page) flatMap { pag =>
- negotiate(
- html = api countFollowing user.id map { nbFollowing =>
- Ok(html.relation.bits.followers(user, pag, nbFollowing))
- },
- api = _ => Ok(jsonRelatedPaginator(pag)).fuccess
- )
+ negotiate(
+ html = notFound,
+ api = _ =>
+ Reasonable(page, 20) {
+ RelatedPager(api.followersPaginatorAdapter(UserModel normalize username), page) flatMap { pag =>
+ Ok(jsonRelatedPaginator(pag)).fuccess
+ }
}
- }
- }
+ )
}
def apiFollowing(name: String) = apiRelation(name, Direction.Following)
diff --git a/app/controllers/User.scala b/app/controllers/User.scala
index 8915fc8fb8..88268eb6c1 100644
--- a/app/controllers/User.scala
+++ b/app/controllers/User.scala
@@ -512,7 +512,7 @@ final class User(
}
}
.sequenceFu
- } yield html.user.opponents(me, relateds)
+ } yield html.relation.bits.opponents(me, relateds)
}
def perfStat(username: String, perfKey: String) =
diff --git a/app/views/relation/bits.scala b/app/views/relation/bits.scala
index 5398a8a859..2d46d33c02 100644
--- a/app/views/relation/bits.scala
+++ b/app/views/relation/bits.scala
@@ -1,44 +1,23 @@
package views.html.relation
+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.game.FavoriteOpponents
import lila.relation.Related
import lila.user.User
-import controllers.routes
-
object bits {
- def followers(u: User, pag: Paginator[Related], nbFollowing: Int)(implicit ctx: Context) =
- layout(s"${u.username} • ${trans.nbFollowers.pluralSameTxt(pag.nbResults)}")(
- div(cls := "box__top")(
- h1(userLink(u, withOnline = false)),
- div(cls := "actions")(
- 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))
- )
+ def friends(u: User, pag: Paginator[Related])(implicit ctx: Context) =
+ layout(s"${u.username} • ${trans.friends.txt()}")(
+ h1(
+ a(href := routes.User.show(u.username), dataIcon := "", cls := "text"),
+ trans.friends()
),
pagTable(pag, routes.Relation.following(u.username))
)
@@ -54,6 +33,38 @@ object bits {
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) =
views.html.base.layout(
title = title,
@@ -71,7 +82,7 @@ object bits {
tr(cls := "paginated")(
td(userLink(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))
)
},
diff --git a/app/views/ublog/form.scala b/app/views/ublog/form.scala
index f1daf43efd..1be22dece1 100644
--- a/app/views/ublog/form.scala
+++ b/app/views/ublog/form.scala
@@ -26,7 +26,7 @@ object form {
) {
main(cls := "page-menu page-small")(
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(),
h1(trans.ublog.newPost()),
etiquette,
@@ -43,7 +43,7 @@ object form {
) {
main(cls := "page-menu page-small")(
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(),
div(cls := "box__top")(
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])(
implicit ctx: Context
) =
postForm(
- cls := "form3",
+ cls := "form3 ublog-post-form__main",
action := post.fold(_ => routes.Ublog.create, p => routes.Ublog.update(p.id.value))
)(
form3.globalError(form),
- post.isRight option form3.split(
- form3.checkbox(
- form("live"),
- trans.ublog.publishOnYourBlog(),
- help = trans.ublog.publishHelp().some,
- half = true
- ),
- form3.group(form("language"), trans.language(), half = true) { field =>
- form3.select(
- field,
- LangList.popularNoRegion.map { l =>
- l.code -> l.toLocale.getDisplayLanguage
+ post.toOption.map { p =>
+ frag(
+ form3.split(
+ form3.group(form("imageAlt"), trans.ublog.imageAlt(), half = true)(form3.input(_)),
+ form3.group(form("imageCredit"), trans.ublog.imageCredit(), half = true)(form3.input(_))
+ )(cls := s"ublog-post-form__image-text ${p.image.isDefined ?? "visible"}"),
+ form3.split(
+ form3.checkbox(
+ form("live"),
+ trans.ublog.publishOnYourBlog(),
+ help = trans.ublog.publishHelp().some,
+ 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("intro"), trans.ublog.postIntro())(form3.input(_)(autofocus)),
form3.group(
diff --git a/app/views/ublog/post.scala b/app/views/ublog/post.scala
index 7ef3e15c96..ec4ddf4e75 100644
--- a/app/views/ublog/post.scala
+++ b/app/views/ublog/post.scala
@@ -44,7 +44,12 @@ object post {
main(cls := "page-menu page-small")(
views.html.blog.bits.menu(none, (if (ctx is user) "mine" else "community").some),
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(),
h1(cls := "ublog-post__title")(post.title),
div(cls := "ublog-post__meta")(
@@ -135,15 +140,15 @@ object post {
)
)(
List(
- ("yes", trans.following, routes.Relation.unfollow _, ""),
- ("no", trans.follow, routes.Relation.follow _, "")
+ ("yes", trans.unfollowX, routes.Relation.unfollow _, ""),
+ ("no", trans.followX, routes.Relation.follow _, "")
).map { case (role, text, route, icon) =>
button(
cls := s"ublog-post__follow__$role button button-big",
dataIcon := icon,
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(
cls := "ublog-post-image",
widthA := size(UblogPost.thumbnail).width,
- heightA := size(UblogPost.thumbnail).height
+ heightA := size(UblogPost.thumbnail).height,
+ alt := post.image.flatMap(_.alt)
)(src := url(post, size))
def url(post: UblogPost.BasePost, size: UblogPost.thumbnail.SizeSelector) =
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")
}
}
diff --git a/app/views/user/opponents.scala b/app/views/user/opponents.scala
deleted file mode 100644
index 8b49eb6709..0000000000
--- a/app/views/user/opponents.scala
+++ /dev/null
@@ -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()))
- )
- )
- )
-}
diff --git a/app/views/user/show/header.scala b/app/views/user/show/header.scala
index 1e1c5b5e29..e3d6d21c39 100644
--- a/app/views/user/show/header.scala
+++ b/app/views/user/show/header.scala
@@ -48,9 +48,6 @@ object header {
),
div(cls := "user-show__social")(
div(cls := "number-menu")(
- a(cls := "nm-item", href := routes.Relation.followers(u.username))(
- splitNumber(trans.nbFollowers.pluralSame(info.nbFollowers))
- ),
u.noBot option a(
href := routes.UserTournament.path(u.username, "recent"),
cls := "nm-item tournament_stats",
@@ -232,6 +229,8 @@ object header {
trans.profileCompletion(s"${profile.completionPercent}%")
),
br,
+ a(href := routes.Relation.following(u.username))(trans.friends()),
+ br,
a(href := routes.User.opponents)(trans.favoriteOpponents())
),
u.playTime.map { playTime =>
diff --git a/bin/mongodb/ublog-image.js b/bin/mongodb/ublog-image.js
new file mode 100644
index 0000000000..64426bc9b4
--- /dev/null
+++ b/bin/mongodb/ublog-image.js
@@ -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 } } }));
diff --git a/modules/api/src/main/PersonalDataExport.scala b/modules/api/src/main/PersonalDataExport.scala
index 1eff5cd6cc..c7c095aa8b 100644
--- a/modules/api/src/main/PersonalDataExport.scala
+++ b/modules/api/src/main/PersonalDataExport.scala
@@ -161,7 +161,7 @@ final class PersonalDataExport(
"title" -> post.title,
"intro" -> post.intro,
"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(", ")
).map { case (k, v) =>
s"$k: $v"
diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala
index 9062665357..69075a4dbd 100644
--- a/modules/i18n/src/main/I18nKeys.scala
+++ b/modules/i18n/src/main/I18nKeys.scala
@@ -277,6 +277,8 @@ val `favoriteOpponents` = new I18nKey("favoriteOpponents")
val `follow` = new I18nKey("follow")
val `following` = new I18nKey("following")
val `unfollow` = new I18nKey("unfollow")
+val `followX` = new I18nKey("followX")
+val `unfollowX` = new I18nKey("unfollowX")
val `block` = new I18nKey("block")
val `blocked` = new I18nKey("blocked")
val `unblock` = new I18nKey("unblock")
@@ -2281,6 +2283,8 @@ val `noPostsInThisBlogYet` = new I18nKey("ublog:noPostsInThisBlogYet")
val `noDrafts` = new I18nKey("ublog:noDrafts")
val `latestBlogPosts` = new I18nKey("ublog:latestBlogPosts")
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 `nbViews` = new I18nKey("ublog:nbViews")
val `viewAllNbPosts` = new I18nKey("ublog:viewAllNbPosts")
diff --git a/modules/irc/src/main/IrcApi.scala b/modules/irc/src/main/IrcApi.scala
index 73f1e4929e..b46c1b2fa3 100644
--- a/modules/irc/src/main/IrcApi.scala
+++ b/modules/irc/src/main/IrcApi.scala
@@ -87,7 +87,7 @@ final class IrcApi(
def chatPanic(mod: Holder, v: Boolean): Funit =
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 =
diff --git a/modules/relation/src/main/RelationApi.scala b/modules/relation/src/main/RelationApi.scala
index a5a7d53fa8..ce2760194e 100644
--- a/modules/relation/src/main/RelationApi.scala
+++ b/modules/relation/src/main/RelationApi.scala
@@ -80,9 +80,8 @@ final class RelationApi(
def fetchAreFriends(u1: ID, u2: ID): Fu[Boolean] =
fetchFollows(u1, u2) >>& fetchFollows(u2, u1)
- private val countFollowingCache = cacheApi[ID, Int](8192, "relation.count.following") {
- _.expireAfterAccess(10 minutes)
- .maximumSize(32768)
+ private val countFollowingCache = cacheApi[ID, Int](8_192, "relation.count.following") {
+ _.maximumSize(8_192)
.buildAsyncFuture { userId =>
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 <=)
- private val countFollowersCache = cacheApi[ID, Int](131_072, "relation.count.followers") {
- _.expireAfterAccess(10 minutes)
- .maximumSize(131_072)
+ private val countFollowersCache = cacheApi[ID, Int](32_768, "relation.count.followers") {
+ _.maximumSize(32_768)
.buildAsyncFuture { userId =>
coll.secondaryPreferred.countSel($doc("u2" -> userId, "r" -> Follow))
}
@@ -147,7 +145,7 @@ final class RelationApi(
repo.follow(u1, u2) >> limitFollow(u1) >>- {
countFollowersCache.update(u2, 1 +)
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")
lila.mon.relation.follow.increment().unit
}
diff --git a/modules/relay/src/main/JsonView.scala b/modules/relay/src/main/JsonView.scala
index 05137760a0..1a653747e6 100644
--- a/modules/relay/src/main/JsonView.scala
+++ b/modules/relay/src/main/JsonView.scala
@@ -84,7 +84,7 @@ object JsonView {
"log" -> s.log.events
) ++
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)
}
}
diff --git a/modules/ublog/src/main/UblogApi.scala b/modules/ublog/src/main/UblogApi.scala
index e015d5c64e..99d6a72817 100644
--- a/modules/ublog/src/main/UblogApi.scala
+++ b/modules/ublog/src/main/UblogApi.scala
@@ -110,10 +110,10 @@ final class UblogApi(
def uploadImage(user: User, post: UblogPost, picture: PicfitApi.FilePart): Fu[UblogPost] =
for {
- image <- picfitApi
- .uploadFile(imageRel(post), picture, userId = user.id)
- _ <- colls.post.update.one($id(post.id), $set("image" -> image.id))
- } yield post.copy(image = image.id.some)
+ pic <- picfitApi.uploadFile(imageRel(post), picture, userId = user.id)
+ image = post.image.fold(UblogImage(pic.id))(_.copy(id = pic.id))
+ _ <- colls.post.updateField($id(post.id), "image", image)
+ } yield post.copy(image = image.some)
def deleteImage(post: UblogPost): Fu[UblogPost] =
picfitApi.deleteByRel(imageRel(post)) >>
diff --git a/modules/ublog/src/main/UblogBsonHandlers.scala b/modules/ublog/src/main/UblogBsonHandlers.scala
index bbd565cb40..bdad0bc8f9 100644
--- a/modules/ublog/src/main/UblogBsonHandlers.scala
+++ b/modules/ublog/src/main/UblogBsonHandlers.scala
@@ -25,6 +25,7 @@ private object UblogBsonHandlers {
.afterRead(_.filter(t => UblogTopic.exists(t.value)))
implicit val langBsonHandler = stringAnyValHandler[Lang](_.code, Lang.apply)
implicit val recordedBSONHandler = Macros.handler[Recorded]
+ implicit val imageBSONHandler = Macros.handler[UblogImage]
implicit val likesBSONHandler = intAnyValHandler[Likes](_.value, Likes)
implicit val viewsBSONHandler = intAnyValHandler[Views](_.value, Views)
implicit val rankBSONHandler = dateIsoHandler[Rank](Iso[DateTime, Rank](Rank, _.value))
diff --git a/modules/ublog/src/main/UblogForm.scala b/modules/ublog/src/main/UblogForm.scala
index 816bffff58..07a220e79a 100644
--- a/modules/ublog/src/main/UblogForm.scala
+++ b/modules/ublog/src/main/UblogForm.scala
@@ -17,14 +17,16 @@ final class UblogForm(markup: UblogMarkup, val captcher: lila.hub.actors.Captche
private val base =
mapping(
- "title" -> cleanNonEmptyText(minLength = 3, maxLength = 80),
- "intro" -> cleanNonEmptyText(minLength = 0, maxLength = 1_000),
- "markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).verifying(markdownImage.constraint),
- "language" -> optional(stringIn(LangList.popularNoRegion.map(_.code).toSet)),
- "topics" -> optional(text),
- "live" -> boolean,
- "gameId" -> text,
- "move" -> text
+ "title" -> cleanNonEmptyText(minLength = 3, maxLength = 80),
+ "intro" -> cleanNonEmptyText(minLength = 0, maxLength = 1_000),
+ "markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).verifying(markdownImage.constraint),
+ "imageAlt" -> optional(cleanNonEmptyText(minLength = 3, maxLength = 200)),
+ "imageCredit" -> optional(cleanNonEmptyText(minLength = 3, maxLength = 200)),
+ "language" -> optional(stringIn(LangList.popularNoRegion.map(_.code).toSet)),
+ "topics" -> optional(text),
+ "live" -> boolean,
+ "gameId" -> text,
+ "move" -> text
)(UblogPostData.apply)(UblogPostData.unapply)
val create = Form(
@@ -37,6 +39,8 @@ final class UblogForm(markup: UblogMarkup, val captcher: lila.hub.actors.Captche
title = post.title,
intro = post.intro,
markdown = post.markdown,
+ imageAlt = post.image.flatMap(_.alt),
+ imageCredit = post.image.flatMap(_.credit),
language = post.language.code.some,
topics = post.topics.map(_.value).mkString(", ").some,
live = post.live,
@@ -52,6 +56,8 @@ object UblogForm {
title: String,
intro: String,
markdown: String,
+ imageAlt: Option[String],
+ imageCredit: Option[String],
language: Option[String],
topics: Option[String],
live: Boolean,
@@ -84,6 +90,9 @@ object UblogForm {
title = title,
intro = intro,
markdown = markdown,
+ image = prev.image.map { i =>
+ i.copy(alt = imageAlt, credit = imageCredit)
+ },
language = LangList.removeRegion(realLanguage | prev.language),
topics = topics ?? UblogTopic.fromStrList,
live = live,
diff --git a/modules/ublog/src/main/UblogPost.scala b/modules/ublog/src/main/UblogPost.scala
index 90a630e1ce..eb3e7097a9 100644
--- a/modules/ublog/src/main/UblogPost.scala
+++ b/modules/ublog/src/main/UblogPost.scala
@@ -13,7 +13,7 @@ case class UblogPost(
intro: String,
markdown: String,
language: Lang,
- image: Option[PicfitImage.Id],
+ image: Option[UblogImage],
topics: List[UblogTopic],
live: Boolean,
created: UblogPost.Recorded,
@@ -28,6 +28,8 @@ case class UblogPost(
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 {
case class Id(value: String) extends AnyVal with StringValue
@@ -50,7 +52,7 @@ object UblogPost {
val blog: UblogBlog.Id
val title: String
val intro: String
- val image: Option[PicfitImage.Id]
+ val image: Option[UblogImage]
val created: Recorded
val lived: Option[Recorded]
def id = _id
@@ -62,7 +64,7 @@ object UblogPost {
blog: UblogBlog.Id,
title: String,
intro: String,
- image: Option[PicfitImage.Id],
+ image: Option[UblogImage],
created: Recorded,
lived: Option[Recorded]
) extends BasePost
diff --git a/public/javascripts/chart/ratingHistory.js b/public/javascripts/chart/ratingHistory.js
index e6aa4a8c23..0b8a90d3e1 100644
--- a/public/javascripts/chart/ratingHistory.js
+++ b/public/javascripts/chart/ratingHistory.js
@@ -2,7 +2,7 @@ lichess.ratingHistoryChart = function (data, { singlePerfName, perfIndex }) {
var oneDay = 86400000;
function smoothDates(data) {
if (!data.length) return [];
-
+
// If last rating wasn't today, add to the array
var today = new Date().setUTCHours(0, 0, 0, 0);
var lastRating = data[data.length - 1];
diff --git a/public/javascripts/study/tour-chapter.js b/public/javascripts/study/tour-chapter.js
index b29bb68a34..6bd15ad2ec 100644
--- a/public/javascripts/study/tour-chapter.js
+++ b/public/javascripts/study/tour-chapter.js
@@ -1,6 +1,6 @@
function loadShepherd(f) {
- var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'default' : 'dark');
- lichess.loadCss('vendor/shepherd/dist/css/' + theme + '.css');
+ var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'dark' : 'default');
+ lichess.loadCss('vendor/' + theme + '.css');
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 () {
f(theme);
diff --git a/public/javascripts/study/tour.js b/public/javascripts/study/tour.js
index 307a85a595..0395751c01 100644
--- a/public/javascripts/study/tour.js
+++ b/public/javascripts/study/tour.js
@@ -1,7 +1,7 @@
function loadShepherd(f) {
if (typeof Shepherd === 'undefined' || Shepherd.activeTour === null) {
- var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'default' : 'dark');
- lichess.loadCss('vendor/shepherd/dist/css/' + theme + '.css');
+ var theme = 'shepherd-theme-' + ($('body').hasClass('dark') ? 'dark' : 'default');
+ lichess.loadCss('vendor/' + theme + '.css');
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 () {
f(theme);
diff --git a/public/vendor/shepherd-theme-dark.css b/public/vendor/shepherd-theme-dark.css
new file mode 100644
index 0000000000..c55ef91f6f
--- /dev/null
+++ b/public/vendor/shepherd-theme-dark.css
@@ -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; }
diff --git a/public/vendor/shepherd-theme-default.css b/public/vendor/shepherd-theme-default.css
new file mode 100644
index 0000000000..c35cecd5bb
--- /dev/null
+++ b/public/vendor/shepherd-theme-default.css
@@ -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; }
diff --git a/translation/source/site.xml b/translation/source/site.xml
index 00f3861f23..5f55f22900 100644
--- a/translation/source/site.xml
+++ b/translation/source/site.xml
@@ -382,6 +382,8 @@ computer analysis, game chat and shareable URL.