rewrite layout

This commit is contained in:
Thibault Duplessis 2018-12-03 15:42:53 +07:00
parent e1bf05c682
commit ecbab40496
12 changed files with 357 additions and 270 deletions

View file

@ -27,10 +27,8 @@ trait I18nHelper {
def i18nFullDbJsObject(db: I18nDb.Ref)(implicit lang: Lang): JsObject =
JsDump.dbToObject(db, lang)
def i18nJsQuantityFunction(implicit lang: Lang): Html = Html(JsQuantity(lang))
private val defaultTimeagoLocale = TimeagoLocales.js.get("en") err "Missing en TimeagoLocales"
def timeagoLocaleScript(implicit ctx: lila.api.Context) = Html {
def timeagoLocaleScript(implicit ctx: lila.api.Context): String = {
TimeagoLocales.js.get(ctx.lang.code) orElse
TimeagoLocales.js.get(ctx.lang.language) getOrElse
defaultTimeagoLocale

View file

@ -2,7 +2,6 @@ package lila.app
package ui
import lila.common.String.html.escapeHtml
import play.twirl.api.Html
case class OpenGraph(
title: String,
@ -14,7 +13,7 @@ case class OpenGraph(
more: List[(String, String)] = Nil
) {
def html = Html(og.str + twitter.str)
def frag = scalatags.Text.RawFrag(s"${og.str}${twitter.str}")
object og {

View file

@ -22,6 +22,8 @@ trait Scalatags {
lazy val dataIcon = attr("data-icon")
lazy val dataHint = attr("data-hint")
lazy val dataHref = attr("data-href")
lazy val dataCount = attr("data-count")
implicit val charAttr = genericAttr[Char]

265
app/views/base/layout.scala Normal file
View file

@ -0,0 +1,265 @@
package views.html.base
import play.twirl.api.Html
import scalatags.Text.all._
import scalatags.Text.{ all => st }
import lila.api.Context
import lila.app.templating.Environment._
import lila.i18n.{ I18nKeys => trans }
import controllers.routes
object layout {
private val fontVersion = 82
private val topComment = raw("""<!-- Lichess is open source! See https://github.com/ornicar/lila -->""")
private val charset = raw("""<meta charset="utf-8">""")
private val fontStylesheets = raw(List(
"""<link href="https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Roboto:300" rel="stylesheet">""",
"""<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:500&text=0123456789:." rel="stylesheet">"""
).mkString)
private val noTranslate = raw("""<meta name="google" content="notranslate" />""")
private val fontPreload = raw(s"""<link rel="preload" href="${staticUrl("font$fontVersion/fonts/lichess.woff")}" as="font" type="font/woff" crossorigin/>""")
private val wasmx = raw("""<meta http-equiv="origin-trial" data-feature="WebAssembly Threads" data-expires="2018-12-12" content="AvQS5g8cLXUfw7Vu3lmQ6B55HURS0KMuY6blwOUmAPYX3Jph8GXMSO4/jTx3el3BxG8SyJRlQTbsRRsjOHwiCAMAAABXeyJvcmlnaW4iOiJodHRwczovL2xpY2hlc3Mub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseVRocmVhZHMiLCJleHBpcnkiOjE1NDQ2MzY2MzN9">""")
private val manifests = raw(List(
"""<link rel="manifest" href="/manifest.json" />""",
"""<meta name="twitter:site" content="@@lichess" />"""
).mkString)
private val faviconSizes = List(256, 128, 64)
private val favicons = raw {
faviconSizes map { px =>
s"""<link rel="icon" type="image/png" href="${staticUrl(s"favicon.$px.png")}" sizes="${px}x${px}"/>"""
} mkString
}
private def titleTag(content: String) = raw(s"""<title>$content</title>""")
private def blindModeForm(implicit ctx: Context) = raw("""<form id="blind_mode" action="${routes.Main.toggleBlindMode}" method="POST"><input type="hidden" name="enable" value="${if(ctx.blindMode) 0 else 1}" /><input type="hidden" name="redirect" value="${ctx.req.path}" /><button type="submit">Accessibility: ${if(ctx.blindMode) "Disable" else "Enable"} blind mode</button></form>""")
private val zenToggle = raw("""<a data-icon="E" id="zentog" class="text fbt active">ZEN MODE</a>""")
private def dasher(me: lila.user.User) = raw(s"""<div class="dasher"><a id="user_tag" class="toggle link">${me.username}</a><div id="dasher_app" class="dropdown"></div></div>""")
private def challenges(implicit ctx: Context) = spaceless(s"""<div class="challenge_notifications">
<a id="challenge_notifications_tag" class="toggle link data-count" data-count="${ctx.nbChallenges}">
<span class="hint--bottom-left" data-hint="${trans.challenges()}"><span data-icon="U"></span></span>
</a>
<div id="challenge_app" class="dropdown"></div>
</div>""")
private def notifications(implicit ctx: Context) = spaceless(s"""<div class="site_notifications">
<a id="site_notifications_tag" class="toggle link data-count" data-count="${ctx.nbNotifications}">
<span class="hint--bottom-left" data-hint="${trans.notifications()}"><span data-icon=""></span></span>
</a>
<div id="notify_app" class="dropdown"></div>
</div>""")
private def anonDasher(playing: Boolean)(implicit ctx: Context) = spaceless(s"""<div class="dasher">
<a class="toggle anon">
<span class="hint--bottom-left" data-hint="${trans.preferences()}"><span data-icon="%"></span></span>
</a>
<div id="dasher_app" class="dropdown" data-playing="$playing"></div>
</div>
<a href="${routes.Auth.login}?referrer=${currentPath}" class="signin button text">${trans.signIn()}</a>""")
private def clinput(implicit ctx: Context) =
raw(s"""<div id="clinput"><a class="link"><span data-icon="y"></span></a><input spellcheck="false" placeholder="${trans.search.txt()}"/></div>""")
private lazy val botImage = img(src := staticUrl("images/icons/bot.png"), title := "Robot chess", style := "display:inline;width:34px;height:34px;vertical-align:top;margin-right:5px;")
private val spaceRegex = """\s{2,}+""".r
private def spaceless(html: String) = raw(spaceRegex.replaceAllIn(html.replace("\\n", ""), ""))
private val dataDev = attr("data-dev")
private val dataUser = attr("data-user")
private val dataSoundSet = attr("data-sound-set")
private val dataSocketDomain = attr("data-socket-domain")
private val dataAssetUrl = attr("data-asset-url")
private val dataAssetVersion = attr("data-asset-version")
private val dataNonce = attr("data-nonce")
private val dataZoom = attr("data-zoom")
private val dataPreload = attr("data-preload")
private val dataPlaying = attr("data-playing")
private val dataPatrons = attr("data-patrons")
private val dataStudying = attr("data-studying")
def apply(
title: String,
fullTitle: Option[String] = None,
baseline: Option[Html] = None,
side: Option[Html] = None,
menu: Option[Html] = None,
chat: Option[Html] = None,
underchat: Option[Html] = None,
robots: Boolean = isGloballyCrawlable,
moreCss: Html = emptyHtml,
moreJs: Html = emptyHtml,
playing: Boolean = false,
openGraph: Option[lila.app.ui.OpenGraph] = None,
atom: Option[Html] = None,
chessground: Boolean = true,
zoomable: Boolean = false,
asyncJs: Boolean = false,
csp: Option[lila.common.ContentSecurityPolicy] = None
)(body: Html)(implicit ctx: Context) =
html(st.lang := ctx.lang.language)(
topComment,
head(
charset,
cspEnabled() option raw(
s"""<meta http-equiv="Content-Security-Policy" content="${csp.getOrElse(defaultCsp)}">"""
),
if (isProd) frag(
wasmxEnabled() option wasmx,
titleTag(fullTitle | s"$title • lichess.org"),
fontStylesheets
)
else frag(
titleTag(s"[dev] ${fullTitle | s"$title • lichess.org"}"),
cssAt("offline/font.noto.css"),
cssAt("offline/font.roboto.mono.css")
),
ctx.currentBg match {
case "dark" => cssTag("dark.css")
case "transp" => cssTags("dark.css", "transp.css")
case _ => emptyHtml
},
cssTag("common.css"),
cssTag("board.css"),
ctx.zoom ifTrue zoomable map { z =>
zoomStyle(z / 100f, ctx.pref.is3d)
},
ctx.pref.is3d option cssTag("board-3d.css"),
ctx.pref.coords == 1 option cssTag("board.coords.inner.css"),
ctx.pageData.inquiry.isDefined option cssTag("inquiry.css"),
ctx.userContext.impersonatedBy.isDefined option cssTag("impersonate.css"),
isStage option cssTag("stage.css"),
moreCss,
link(id := "piece-sprite", href := assetUrl(s"stylesheets/piece/${ctx.currentPieceSet}.css"), `type` := "text/css", rel := "stylesheet"),
meta(content := openGraph.fold(trans.siteDescription.txt())(o => o.description), name := "description"),
link(id := "favicon", rel := "shortcut icon", href := staticUrl("images/favicon-32-white.png"), `type` := "image/x-icon"),
link(rel := "mask-icon", href := staticUrl("favicon.svg"), color := "black"),
favicons,
!robots option raw("""<meta content="noindex, nofollow" name="robots">"""),
noTranslate,
openGraph.map(_.frag),
atom | link(href := routes.Blog.atom, `type` := "application/atom+xml", rel := "alternate", st.title := trans.blog.txt()),
ctx.transpBgImg map { img =>
raw(s"""<style type="text/css" id="bg-data">body.transp::before{background-image:url('$img');}</style>""")
},
fontPreload,
manifests
),
st.body(
cls := List(
"preload base" -> true,
ctx.currentTheme.cssClass -> true,
ctx.currentTheme3d.cssClass -> true,
(if (ctx.currentBg == "transp") "dark transp" else ctx.currentBg) -> true,
ctx.currentPieceSet3d.toString -> true,
"piece_letter" -> ctx.pref.pieceNotationIsLetter,
"zen" -> ctx.pref.isZen,
"blind_mode" -> ctx.blindMode,
"kid" -> ctx.kid,
"mobile" -> ctx.isMobileBrowser,
"playing fixed-scroll" -> playing
),
dataDev := (!isProd).option("true"),
dataUser := ctx.userId,
dataSoundSet := ctx.currentSoundSet.toString,
dataSocketDomain := socketDomain,
dataAssetUrl := assetBaseUrl,
dataAssetVersion := assetVersion.value,
dataNonce := ctx.nonce.map(_.value),
dataZoom := ctx.zoom.map(_.toString)
)(
blindModeForm,
div(id := "site_description")(trans.siteDescription()),
ctx.pageData.inquiry map { views.html.mod.inquiry(_) },
ctx.me ifTrue ctx.userContext.impersonatedBy.isDefined map { views.html.mod.impersonate(_) },
isStage option div(id := "stage")(
"This is an empty lichess preview website for developers. ",
a(href := "https://lichess.org")("Go to lichess.org instead")
),
lila.security.EmailConfirm.cookie.get(ctx.req).map(views.html.auth.emailConfirmBanner(_)),
playing option zenToggle,
div(id := "top", cls := (if (ctx.pref.is3d) "is3d" else "is2d"))(
topmenu(),
div(id := "ham-plate", cls := "link hint--bottom", dataHint := trans.menu.txt())(
div(id := "hamburger", dataIcon := "[")
),
ctx.me map { me =>
frag(dasher(me), challenges, notifications)
} getOrElse {
!ctx.pageData.error option anonDasher(playing)
},
ctx.teamNbRequests > 0 option
a(cls := "link data-count", href := routes.Team.requests, dataCount := ctx.teamNbRequests)(
span(cls := "hint--bottom-left", dataHint := trans.teams.txt())(span(dataIcon := "f"))
),
isGranted(_.SeeReport) option
a(cls := "link text data-count", href := routes.Report.list, dataCount := reportNbOpen, dataIcon := ""),
clinput,
a(id := "reconnecting", cls := "link text", dataIcon := "B")(trans.reconnecting())
),
div(cls := s"content ${if (ctx.pref.is3d) "is3d" else "is2d"}")(
div(id := "site_header")(
div(id := "notifications"),
div(cls := "board_left")(
h1(
a(id := "site_title", href := routes.Lobby.home)(
if (ctx.kid) span(st.title := trans.kidMode.txt(), cls := "kiddo")("😊")
else ctx.isBot option botImage,
"lichess",
span(cls := "extension")(if (isProd) ".org" else " dev")
)
),
baseline,
menu map { sideMenu =>
div(cls := "side_menu")(sideMenu)
},
side,
chat
),
underchat map { g =>
div(cls := "under_chat")(g)
}
),
div(id := "lichess")(body)
),
ctx.me.map { me =>
div(
id := "friend_box",
dataPreload := ctx.onlineFriends.users.map(_.titleName).mkString(","),
dataPlaying := ctx.onlineFriends.playing.mkString(","),
dataPatrons := ctx.onlineFriends.patrons.mkString(","),
dataStudying := ctx.onlineFriends.studying.mkString(",")
)(
div(cls := "title")(
strong(cls := "online")(" "),
" ",
trans.onlineFriends()
),
div(cls := "content_wrap")(
div(cls := "content list")(
div(cls := List(
"nobody" -> true,
"none" -> ctx.onlineFriends.users.nonEmpty
))(
span(trans.noFriendsOnline()),
a(cls := "find button", href := routes.User.opponents)(
span(cls := "is3 text", dataIcon := "h")(trans.findFriends())
)
)
)
)
)
},
chessground option jsTag("vendor/chessground.min.js"),
ctx.requiresFingerprint option fingerprintTag,
jsAt(s"compiled/lichess.site${isProd ?? ".min"}.js", async = asyncJs),
moreJs,
embedJs(s"""lichess.quantity=${lila.i18n.JsQuantity(ctx.lang)};$timeagoLocaleScript;"""),
ctx.pageData.inquiry.isDefined option jsTag("inquiry.js", async = asyncJs)
)
)
}

View file

@ -1,204 +0,0 @@
@(
title: String,
fullTitle: Option[String] = None,
baseline: Option[Html] = None,
side: Option[Html] = None,
menu: Option[Html] = None,
chat: Option[Html] = None,
underchat: Option[Html] = None,
robots: Boolean = isGloballyCrawlable,
moreCss: Html = emptyHtml,
moreJs: Html = emptyHtml,
playing: Boolean = false,
openGraph: Option[lila.app.ui.OpenGraph] = None,
atom: Option[Html] = None,
chessground: Boolean = true,
zoomable: Boolean = false,
asyncJs: Boolean = false,
csp: Option[lila.common.ContentSecurityPolicy] = None)(body: Html)(implicit ctx: Context)
<!doctype html>
<html lang="@lang.language">
<!-- Lichess is open source! See https://github.com/ornicar/lila -->
<head>
<meta charset="utf-8">
@if(cspEnabled()) {
<meta http-equiv="Content-Security-Policy" content="@csp.getOrElse(defaultCsp)">
}
@if(isProd) {
@if(wasmxEnabled()) {
<meta http-equiv="origin-trial" data-feature="WebAssembly Threads" data-expires="2018-12-12" content="AvQS5g8cLXUfw7Vu3lmQ6B55HURS0KMuY6blwOUmAPYX3Jph8GXMSO4/jTx3el3BxG8SyJRlQTbsRRsjOHwiCAMAAABXeyJvcmlnaW4iOiJodHRwczovL2xpY2hlc3Mub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseVRocmVhZHMiLCJleHBpcnkiOjE1NDQ2MzY2MzN9">
}
<title>@fullTitle.getOrElse{@title • lichess.org}</title>
<link href="https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Roboto:300" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto+Mono:500&text=0123456789:." rel="stylesheet">
} else {
<title>[dev] @fullTitle.getOrElse{@title • lichess.org}</title>
@cssAt("offline/font.noto.css")
@cssAt("offline/font.roboto.mono.css")
}
@ctx.currentBg match {
case "dark" => { @cssTag("dark.css") }
case "transp" => { @cssTag("dark.css")@cssTag("transp.css") }
case _ => {}
}
@cssTag("common.css")
@cssTag("board.css")
@if(zoomable) {
@ctx.zoom.map { z =>
@zoomStyle(z / 100f, ctx.pref.is3d) }
}
@if(ctx.pref.is3d) { @cssTag("board-3d.css") }
@if(ctx.pref.coords == 1) { @cssTag("board.coords.inner.css") }
@if(ctx.pageData.inquiry.isDefined) { @cssTag("inquiry.css") }
@if(ctx.userContext.impersonatedBy.isDefined) { @cssTag("impersonate.css") }
@if(isStage) { @cssTag("stage.css") }
@moreCss
<link id="piece-sprite" href="@assetUrl(s"stylesheets/piece/${ctx.currentPieceSet}.css")" type="text/css" rel="stylesheet"/>
<meta content="@openGraph.fold(trans.siteDescription.txt())(o => o.description)" name="description">
<link id="favicon" rel="shortcut icon" href="@staticUrl("images/favicon-32-white.png")" type="image/x-icon" />
<link rel="mask-icon" href="@staticUrl("favicon.svg")" color="black">
<link rel="icon" type="image/png" href="@staticUrl("favicon.256.png")" sizes="256x256"/><link rel="icon" type="image/png" href="@staticUrl("favicon.128.png")" sizes="128x128"/><link rel="icon" type="image/png" href="@staticUrl("favicon.64.png")" sizes="64x64"/>
@if(!robots) {<meta content="noindex, nofollow" name="robots">}
<meta name="google" content="notranslate" />
@openGraph.map(_.html)
@atom.getOrElse {
<link href="@routes.Blog.atom()" type="application/atom+xml" rel="alternate" title="@trans.blog()" />
}
@ctx.transpBgImg.map { img =>
<style type="text/css" id="bg-data">body.transp::before{background-image:url('@img');}</style>
}
<link rel="preload" href="@staticUrl("font82/fonts/lichess.woff")" as="font" type="font/woff" crossorigin/>
<link rel="manifest" href="/manifest.json" />
<meta name="twitter:site" content="@@lichess" />
</head>
<body class="preload base @List(
ctx.currentTheme.cssClass,
ctx.currentTheme3d.cssClass,
if (ctx.currentBg == "transp") "dark transp" else ctx.currentBg,
ctx.currentPieceSet3d,
ctx.pref.pieceNotationIsLetter ?? "piece_letter",
ctx.pref.isZen ?? "zen",
ctx.blindMode ?? "blind_mode",
ctx.kid ?? "kid",
ctx.isMobileBrowser ?? "mobile",
playing ?? "playing fixed-scroll").mkString(" ")"
@if(!isProd){data-dev="true"}
@ctx.userId.map {id => data-user="@id"}
data-sound-set="@ctx.currentSoundSet"
data-socket-domain="@socketDomain"
data-asset-url="@assetBaseUrl"
data-asset-version="@assetVersion"
@ctx.nonce.map { nonce => data-nonce="@nonce" }
@ctx.zoom.map { zoom => data-zoom="@zoom" }>
<form id="blind_mode" action="@routes.Main.toggleBlindMode" method="POST"><input type="hidden" name="enable" value="@if(ctx.blindMode){0}else{1}" /><input type="hidden" name="redirect" value="@ctx.req.path" /><button type="submit">Accessibility: @if(ctx.blindMode){Disable}else{Enable} blind mode</button></form>
<div id="site_description">@trans.siteDescription()</div>
@ctx.pageData.inquiry.map { inquiry =>
@mod.inquiry(inquiry)
}
@ctx.me.map { me =>
@if(ctx.userContext.impersonatedBy.isDefined) { @mod.impersonate(me) }
}
@if(isStage) {
<div id="stage">This is an empty lichess preview website for developers. <a href="https://lichess.org">Go to lichess.org instead</a></div>
}
@lila.security.EmailConfirm.cookie.get(ctx.req).map(auth.emailConfirmBanner(_))
@if(playing) { <a data-icon="E" id="zentog" class="text fbt active">ZEN MODE</a> }
<div id="top" class="@if(ctx.pref.is3d){is3d}else{is2d}">
@topmenu()
<div id="ham-plate" class="link hint--bottom" data-hint="@trans.menu()">
<div id="hamburger" data-icon="["></div>
</div>
@ctx.me.map { me =>
<div class="dasher">
<a id="user_tag" class="toggle link">@me.username</a>
<div id="dasher_app" class="dropdown"></div>
</div>
<div class="challenge_notifications">
<a id="challenge_notifications_tag" class="toggle link data-count" data-count="@ctx.nbChallenges">
<span class="hint--bottom-left" data-hint="@trans.challenges()">
<span data-icon="U"></span>
</span>
</a>
<div id="challenge_app" class="dropdown"></div>
</div>
<div class="site_notifications">
<a id="site_notifications_tag" class="toggle link data-count" data-count="@ctx.nbNotifications">
<span class="hint--bottom-left" data-hint="@trans.notifications()">
<span data-icon=""></span>
</span>
</a>
<div id="notify_app" class="dropdown"></div>
</div>
}.getOrElse {
@if(!ctx.pageData.error) {
<div class="dasher">
<a class="toggle anon">
<span class="hint--bottom-left" data-hint="@trans.preferences()">
<span data-icon="%"></span>
</span>
</a>
<div id="dasher_app" class="dropdown"@if(playing){ data-playing="true"}></div>
</div>
<a href="@routes.Auth.login?referrer=@currentPath" class="signin button text">@trans.signIn()</a>
}
}
@if(ctx.teamNbRequests > 0) {
<a class="link data-count" href="@routes.Team.requests()" data-count="@ctx.teamNbRequests">
<span class="hint--bottom-left" data-hint="@trans.teams()">
<span data-icon="f"></span>
</span>
</a>
}
@if(isGranted(_.SeeReport)) {
<a class="link text data-count" href="@routes.Report.list" data-count="@reportNbOpen" data-icon=""></a>
}
<div id="clinput"><a class="link"><span data-icon="y"></span></a><input spellcheck="false" placeholder="@trans.search()"/></div>
<a id="reconnecting" class="link text" data-icon="B">@trans.reconnecting()</a>
</div>
<div class="content @if(ctx.pref.is3d){is3d}else{is2d}">
<div id="site_header">
<div id="notifications"></div>
<div class="board_left">
<h1>
<a id="site_title" href="@routes.Lobby.home">@if(ctx.kid){<span title="@trans.kidMode()" class="kiddo">😊</span>}else{@if(ctx.isBot){<img src="@staticUrl("images/icons/bot.png")" title="Robot chess" style="display:inline;width:34px;height:34px;vertical-align:top;margin-right:5px;"/>}}lichess<span class="extension">@if(isProd){.org}else{ dev}</span></a>
</h1>
@baseline
@menu.map { sideMenu =>
<div class="side_menu">@sideMenu</div>
}
@side
@chat
</div>
@underchat.map { g =>
<div class="under_chat">@g</div>
}
</div>
<div id="lichess">
@body
</div>
</div>
@ctx.me.map { me =>
<div id="friend_box" data-preload="@ctx.onlineFriends.users.map(_.titleName).mkString(",")"
data-playing="@ctx.onlineFriends.playing.mkString(",")"
data-patrons="@ctx.onlineFriends.patrons.mkString(",")"
data-studying="@ctx.onlineFriends.studying.mkString(",")">
<div class="title"><strong class="online"> </strong> @trans.onlineFriends()</div>
<div class="content_wrap">
<div class="content list"></div>
<div class="nobody@if(ctx.onlineFriends.users.nonEmpty){ none}">
<span>@trans.noFriendsOnline()</span>
<a class="find button" href="@routes.User.opponents">
<span class="is3 text" data-icon="h">@trans.findFriends()</span>
</a>
</div>
</div>
</div>
}
@if(chessground) {@jsTag("vendor/chessground.min.js")}
@if(ctx.requiresFingerprint) { @fingerprintTag }
@jsAt(s"compiled/lichess.site${isProd??".min"}.js", async = asyncJs)
@moreJs
@embedJs {lichess.quantity=@i18nJsQuantityFunction;@timeagoLocaleScript;}
@if(ctx.pageData.inquiry.isDefined) { @jsTag("inquiry.js", async = asyncJs) }
</body>
</html>

View file

@ -0,0 +1,71 @@
package views.html.base
import scalatags.Text.all._
import scalatags.Text.tags2.section
import lila.api.Context
import lila.app.templating.Environment._
import lila.i18n.{ I18nKeys => trans }
import controllers.routes
object topmenu {
def apply()(implicit ctx: Context) = div(id := "topmenu", cls := "hover")(
section(
a(href := "/")(trans.play()),
div(
if (ctx.noBot) a(href := "/?any#hook")(trans.createAGame())
else a(href := "/?any#friend")(trans.playWithAFriend()),
ctx.noBot option frag(
a(href := routes.Tournament.home())(trans.tournament()),
a(href := routes.Simul.home)(trans.simultaneousExhibitions())
)
)
),
section(
a(href := "@routes.Puzzle.home")(trans.learnMenu()),
div(
ctx.noBot option frag(
a(href := routes.Learn.index)(trans.chessBasics()),
a(href := routes.Puzzle.home)(trans.training()),
a(href := routes.Practice.index)("Practice"),
a(href := routes.Coordinate.home)(trans.coordinates.coordinates())
),
a(href := routes.Study.allDefault(1))("Study"),
a(href := routes.Coach.allDefault(1))(trans.coaches())
)
),
section(
a(href := "@routes.Tv.index")(trans.watch()),
div(
a(href := routes.Tv.index)("Lichess TV"),
a(href := routes.Tv.games)(trans.currentGames()),
a(href := routes.Streamer.index())("Streamers"),
a(href := routes.Relay.index())("Broadcasts (beta)"),
ctx.noBot option a(href := routes.Video.index)("@trans.videoLibrary()")
)
),
section(
a(href := "@routes.User.list")(trans.community()),
div(
a(href := routes.User.list)(trans.players()),
NotForKids(frag(
a(href := routes.Team.home())(trans.teams()),
a(href := routes.ForumCateg.index)(trans.forum())
)),
a(href := routes.QaQuestion.index())(trans.questionsAndAnswers())
)
),
section(
a(href := "@routes.UserAnalysis.index")(trans.tools()),
div(
a(href := routes.UserAnalysis.index)(trans.analysis()),
a(href := s"${routes.UserAnalysis.index}#explorer")(trans.openingExplorer()),
a(href := routes.Editor.index)(trans.boardEditor()),
a(href := routes.Importer.importGame)(trans.importGame()),
a(href := routes.Search.index())(trans.advancedSearch())
)
)
)
}

View file

@ -1,54 +0,0 @@
@()(implicit ctx: Context)
<div id="topmenu" class="hover">
<section>
<a href="/">@trans.play()</a>
<div>
@if(ctx.noBot){<a href="/?any#hook">@trans.createAGame()</a>}else{<a href="/?any#friend">@trans.playWithAFriend()</a>}
@if(ctx.noBot){<a href="@routes.Tournament.home()">@trans.tournament()</a>}
@if(ctx.noBot){<a href="@routes.Simul.home">@trans.simultaneousExhibitions()</a>}
</div>
</section>
<section>
<a href="@routes.Puzzle.home">@trans.learnMenu()</a>
<div>
@if(ctx.noBot){<a href="@routes.Learn.index">@trans.chessBasics()</a>}
@if(ctx.noBot){<a href="@routes.Puzzle.home">@trans.training()</a>}
@if(ctx.noBot){<a href="@routes.Practice.index">Practice</a>}
@if(ctx.noBot){<a href="@routes.Coordinate.home">@trans.coordinates.coordinates()</a>}
<a href="@routes.Study.allDefault(1)">Study</a>
<a href="@routes.Coach.allDefault(1)">@trans.coaches()</a>
</div>
</section>
<section>
<a href="@routes.Tv.index">@trans.watch()</a>
<div>
<a href="@routes.Tv.index">Lichess TV</a>
<a href="@routes.Tv.games">@trans.currentGames()</a>
<a href="@routes.Streamer.index()">Streamers</a>
<a href="@routes.Relay.index()">Broadcasts (beta)</a>
@if(ctx.noBot){<a href="@routes.Video.index">@trans.videoLibrary()</a>}
</div>
</section>
<section>
<a href="@routes.User.list">@trans.community()</a>
<div>
<a href="@routes.User.list">@trans.players()</a>
@NotForKids {
<a href="@routes.Team.home()">@trans.teams()</a>
<a href="@routes.ForumCateg.index">@trans.forum()</a>
}
<a href="@routes.QaQuestion.index()">@trans.questionsAndAnswers()</a>
</div>
</section>
<section>
<a href="@routes.UserAnalysis.index">@trans.tools()</a>
<div>
<a href="@routes.UserAnalysis.index">@trans.analysis()</a>
<a href="@routes.UserAnalysis.index#explorer">@trans.openingExplorer()</a>
<a href="@routes.Editor.index">@trans.boardEditor()</a>
<a href="@routes.Importer.importGame">@trans.importGame()</a>
<a href="@routes.Search.index()">@trans.advancedSearch()</a>
</div>
</section>
</div>

View file

@ -16,7 +16,9 @@ object recent {
a(dataIcon := p.isTeam.option("f"), cls := "post_link", href := routes.ForumPost.redirect(p.postId), title := p.topicName)(
shorten(p.topicName, 30)
),
" ",
userIdLink(p.userId, withOnline = false),
" ",
span(cls := "extract")(shorten(p.text, 70))
)
}

View file

@ -14,6 +14,8 @@ import controllers.routes
object bits {
private val dataTitle = attr("data-title")
def featuredJs(pov: Pov) = Html {
s"""${gameFenNoCtx(pov, tv = true)}${vstext(pov)(none)}"""
}
@ -75,7 +77,7 @@ object bits {
private def playerTitle(player: Player) =
lightUser(player.userId).flatMap(_.title) map { t =>
span(cls := "title", attr("data-title") := t, title := Title titleName Title(t))(t)
span(cls := "title", dataTitle := t, title := Title titleName Title(t))(t)
}
def vstext(pov: Pov)(ctxOption: Option[Context]) =

View file

@ -12,6 +12,8 @@ import controllers.routes
object side {
private val separator = " • "
private val dataUserTv = attr("data-user-tv")
private val dataTime = attr("data-time")
def apply(
pov: lila.game.Pov,
@ -103,7 +105,7 @@ object side {
userTv.map { u =>
div(cls := "side_box")(
h2(cls := "top user_tv text", attr("data-user-tv") := u.id, dataIcon := "1")(u.titleUsername)
h2(cls := "top user_tv text", dataUserTv := u.id, dataIcon := "1")(u.titleUsername)
)
} orElse {
lila.common.HTTPRequest.isMobile(ctx.req) option
@ -116,7 +118,7 @@ object side {
tour.map { t =>
div(cls := "game_tournament side_box no_padding scroll-shadow-soft")(
p(cls := "top text", dataIcon := "g")(a(href := routes.Tournament.show(t.id))(t.fullName)),
div(cls := "clock", attr("data-time") := t.secondsToFinish)(
div(cls := "clock", dataTime := t.secondsToFinish)(
div(cls := "time")(t.clockStatus)
)
)

View file

@ -53,7 +53,7 @@ object home {
simuls.find(_.spotlightable) take 2 map { views.html.simul.homepageSpotlight(_) } toList
),
ctx.me map { u =>
div(id := "timeline", attr("data-href") := routes.Timeline.home)(
div(id := "timeline", dataHref := routes.Timeline.home)(
views.html.timeline entries userTimeline,
div(cls := "links")(
userTimeline.size >= 8 option

View file

@ -12,12 +12,16 @@ import controllers.routes
object bits {
private val dataColor = attr("data-color")
private val dataFen = attr("data-fen")
private val dataLastmove = attr("data-lastmove")
def daily(p: lila.puzzle.Puzzle, fen: String, lastMove: String) = a(
href := routes.Puzzle.daily(),
cls := "mini_board parse_fen is2d",
attr("data-color") := p.color.name,
attr("data-fen") := fen,
attr("data-lastmove") := lastMove
dataColor := p.color.name,
dataFen := fen,
dataLastmove := lastMove
)(miniBoardContent)
def jsI18n(implicit context: Context) = toJson(i18nJsObject(translations))