streamers WIP
parent
0fca0bcf99
commit
384a861095
|
@ -49,7 +49,7 @@ final class Env(
|
|||
getRatingChart = Env.history.ratingChartApi.apply,
|
||||
getRanks = Env.user.cached.ranking.getAll,
|
||||
isHostingSimul = Env.simul.isHosting,
|
||||
fetchIsStreamer = Env.tv.isStreamer.apply,
|
||||
fetchIsStreamer = Env.streamer.api.isStreamer,
|
||||
fetchTeamIds = Env.team.cached.teamIdsList,
|
||||
fetchIsCoach = Env.coach.api.isListedCoach,
|
||||
insightShare = Env.insight.share,
|
||||
|
|
|
@ -51,8 +51,8 @@ object Streamer extends LilaController {
|
|||
AsStreamer { s =>
|
||||
implicit val req = ctx.body
|
||||
StreamerForm.userForm(s.streamer).bindFromRequest.fold(
|
||||
_ => fuccess(BadRequest),
|
||||
data => api.update(s.streamer, data) inject Ok
|
||||
error => BadRequest(html.streamer.edit(s, error)).fuccess,
|
||||
data => api.update(s.streamer, data) inject Redirect(routes.Streamer.edit())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,12 +50,6 @@ case class UserInfo(
|
|||
user = user.id,
|
||||
kind = Trophy.Kind.Developer,
|
||||
date = org.joda.time.DateTime.now
|
||||
),
|
||||
isStreamer option Trophy(
|
||||
_id = "",
|
||||
user = user.id,
|
||||
kind = Trophy.Kind.Streamer,
|
||||
date = org.joda.time.DateTime.now
|
||||
)
|
||||
).flatten ::: trophies
|
||||
|
||||
|
@ -131,9 +125,9 @@ object UserInfo {
|
|||
postApi: PostApi,
|
||||
studyRepo: lila.study.StudyRepo,
|
||||
getRatingChart: User => Fu[Option[String]],
|
||||
getRanks: String => Fu[Map[String, Int]],
|
||||
isHostingSimul: String => Fu[Boolean],
|
||||
fetchIsStreamer: String => Fu[Boolean],
|
||||
getRanks: User.ID => Fu[Map[String, Int]],
|
||||
isHostingSimul: User.ID => Fu[Boolean],
|
||||
fetchIsStreamer: User => Fu[Boolean],
|
||||
fetchTeamIds: User.ID => Fu[List[String]],
|
||||
fetchIsCoach: User => Fu[Boolean],
|
||||
insightShare: lila.insight.Share,
|
||||
|
@ -150,7 +144,7 @@ object UserInfo {
|
|||
shieldApi.active(user) zip
|
||||
fetchTeamIds(user.id) zip
|
||||
fetchIsCoach(user) zip
|
||||
fetchIsStreamer(user.id) zip
|
||||
fetchIsStreamer(user) zip
|
||||
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
|
||||
getPlayTime(user) zip
|
||||
completionRate(user.id) flatMap {
|
||||
|
|
|
@ -21,6 +21,13 @@ trait FormHelper { self: I18nHelper =>
|
|||
errors map errMsg mkString
|
||||
}
|
||||
|
||||
def errMsgMaterial(errors: Seq[FormError])(implicit ctx: Context): Option[Html] = errors.nonEmpty option Html {
|
||||
val msgs = errors.map { error =>
|
||||
s"""<p class="error">${transKey(error.message, I18nDb.Site, error.args)}</p>"""
|
||||
} mkString ""
|
||||
s"""<div class="form-group has-error">$msgs</div>"""
|
||||
}
|
||||
|
||||
def globalError(form: Form[_])(implicit ctx: Context): Option[Html] =
|
||||
form.globalError map errMsg
|
||||
|
||||
|
|
|
@ -7,11 +7,7 @@ back = true,
|
|||
moreCss = cssTag("streamer.form.css").some) {
|
||||
<form class="streamer-new" action="@routes.Streamer.createApply" method="POST">
|
||||
<h2>Are you ready to become a lichess streamer, @me.username?</h2>
|
||||
<ul>
|
||||
<li>You will be listed on <a href="@routes.Streamer.index()">the lichess streamer directory</a>.</li>
|
||||
<li>When you stream with the keyword "lichess.org" in the stream title, you will be bumped up the top of the list!</li>
|
||||
<li>You are free to stream on other chess servers, or to stream non-chess things; just don't include "lichess.org" in the stream title then.</li>
|
||||
</ul>
|
||||
@rules()
|
||||
<p>
|
||||
<button type="submit" class="submit button large new text" data-icon="">Here we go!</button>
|
||||
</p>
|
||||
|
|
|
@ -40,16 +40,25 @@ side = side.some) {
|
|||
}
|
||||
</div>
|
||||
<div class="overview">
|
||||
<h1>
|
||||
@s.user.title.map { t => @t }@s.user.profileOrDefault.nonEmptyRealName.getOrElse(s.user.username)
|
||||
</h1>
|
||||
<h1>Lichess Streamer</h1>
|
||||
@rules()
|
||||
</div>
|
||||
</div>
|
||||
<form class="content_box_content material form" action="@routes.Streamer.edit" method="POST">
|
||||
@base.form.group(form("profile.headline"), Html("Short and inspiring headline"), help = Html("Just one sentence to make students want to choose you").some) {
|
||||
@base.form.input(form("profile.headline"), maxLength = 170)
|
||||
@errMsgMaterial(form.errors)
|
||||
@base.form.group(form("twitch"), Html("Your Twitch username or URL"), help = Html("Optional. Leave empty if none").some) {
|
||||
@base.form.input(form("twitch"))
|
||||
}
|
||||
<div class="status text" data-icon="E">Your changes have been saved.</div>
|
||||
@base.form.group(form("youTube"), Html("Your YouTube channel ID or URL"), help = Html("Optional. Leave empty if none").some) {
|
||||
@base.form.input(form("youTube"))
|
||||
}
|
||||
@base.form.group(form("name"), Html("Your streamer name"), help = Html("Keep it short: 20 characters max").some) {
|
||||
@base.form.input(form("name"), maxLength = 20)
|
||||
}
|
||||
@base.form.group(form("description"), Html("Short description"), help = Html("Who are you, why do you love streaming?").some) {
|
||||
<textarea name="description" id="description" maxlength="300">@form("description").value</textarea>
|
||||
}
|
||||
@base.form.submit()
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
moreCss = cssTag("streamer.list.css"),
|
||||
moreJs = jsTag("vendor/jquery.infinitescroll.min.js")) {
|
||||
<div class="content_box no_padding streamers">
|
||||
<div class="top"><h1>Streamers</h1></div>
|
||||
<div class="top"><h1 data-icon="" class="text">Streamers</h1></div>
|
||||
<div class="list infinitescroll">
|
||||
@pager.currentPageResults.map { c =>
|
||||
<div class="streamer paginated_element" data-dedup="@c.streamer.id">
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
@()
|
||||
<ul>
|
||||
<li>You will be listed on <a href="@routes.Streamer.index()">the lichess streamer directory</a>.</li>
|
||||
<li>When you stream with the keyword "lichess.org" in the stream title, you will be bumped up the top of the list!</li>
|
||||
<li>You are free to stream on other chess servers, or to stream non-chess things; just don't include "lichess.org" in the stream title then.</li>
|
||||
</ul>
|
|
@ -3,33 +3,33 @@
|
|||
@pic(s, 250)
|
||||
@defining(s.user.profileOrDefault) { profile =>
|
||||
<div class="overview">
|
||||
<h1>
|
||||
@s.user.title.map { t => @t }@s.user.realNameOrUsername
|
||||
</h1>
|
||||
<h1>@s.streamer.name</h1>
|
||||
@s.streamer.description.map(_.value).map { d =>
|
||||
<p class="headline @if(d.size < 60){small} else {@if(d.size < 120){medium}else{large}}">@d</p>
|
||||
}
|
||||
@s.streamer.twitch.map { twitch =>
|
||||
<div class="service twitch" href="@twitch.fullUrl">
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.089 0L.525 4.175v16.694h5.736V24h3.132l3.127-3.132h4.695l6.26-6.258V0H2.089zm2.086 2.085H21.39v11.479l-3.652 3.652H12l-3.127 3.127v-3.127H4.175V2.085z"/><path d="M9.915 12.522H12v-6.26H9.915v6.26zm5.735 0h2.086v-6.26H15.65v6.26z"/>
|
||||
</svg>
|
||||
@twitch.minUrl
|
||||
</div>
|
||||
}
|
||||
@s.streamer.youTube.map { youTube =>
|
||||
<div class="service youTube" href="@youTube.fullUrl">
|
||||
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path class="a" d="M23.495 6.205a3.007 3.007 0 0 0-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 0 0 .527 6.205a31.247 31.247 0 0 0-.522 5.805 31.247 31.247 0 0 0 .522 5.783 3.007 3.007 0 0 0 2.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 0 0 2.088-2.088 31.247 31.247 0 0 0 .5-5.783 31.247 31.247 0 0 0-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/>
|
||||
</svg>
|
||||
@youTube.minUrl
|
||||
<div class="services">
|
||||
@s.streamer.twitch.map { twitch =>
|
||||
<div class="service twitch" href="@twitch.fullUrl">
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.089 0L.525 4.175v16.694h5.736V24h3.132l3.127-3.132h4.695l6.26-6.258V0H2.089zm2.086 2.085H21.39v11.479l-3.652 3.652H12l-3.127 3.127v-3.127H4.175V2.085z"/><path d="M9.915 12.522H12v-6.26H9.915v6.26zm5.735 0h2.086v-6.26H15.65v6.26z"/>
|
||||
</svg>
|
||||
@twitch.minUrl
|
||||
</div>
|
||||
}
|
||||
@s.streamer.youTube.map { youTube =>
|
||||
<div class="service youTube" href="@youTube.fullUrl">
|
||||
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path class="a" d="M23.495 6.205a3.007 3.007 0 0 0-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 0 0 .527 6.205a31.247 31.247 0 0 0-.522 5.805 31.247 31.247 0 0 0 .522 5.783 3.007 3.007 0 0 0 2.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 0 0 2.088-2.088 31.247 31.247 0 0 0 .5-5.783 31.247 31.247 0 0 0-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/>
|
||||
</svg>
|
||||
@youTube.minUrl
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@s.streamer.seenAt.map { seenAt =>
|
||||
@trans.lastSeenActive(momentFromNow(seenAt))
|
||||
<p class="at">@trans.lastSeenActive(momentFromNow(seenAt))</p>
|
||||
}
|
||||
@s.streamer.liveAt.map { liveAt =>
|
||||
Last stream @momentFromNow(liveAt)
|
||||
}
|
||||
<p class="at">Last stream @momentFromNow(liveAt)</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -57,5 +57,9 @@
|
|||
}
|
||||
@if(info.isCoach) {
|
||||
<a href="@routes.Coach.show(u.username)"
|
||||
class="trophy award icon3d coach hint--left" data-hint="Lichess coach">:</a>
|
||||
class="trophy award icon3d coach hint--left" data-hint="Lichess Coach">:</a>
|
||||
}
|
||||
@if(info.isStreamer) {
|
||||
<a href="@routes.Streamer.show(u.username)"
|
||||
class="trophy award icon3d streamer hint--left" data-hint="Lichess Streamer"></a>
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package lila.streamer
|
||||
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import lila.db.dsl._
|
||||
import reactivemongo.bson._
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.util.{ Try, Success, Failure }
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.user.UserRepo
|
||||
|
||||
private final class Importer(api: StreamerApi, flagColl: Coll) {
|
||||
|
||||
import Streamer._
|
||||
|
@ -13,21 +15,27 @@ private final class Importer(api: StreamerApi, flagColl: Coll) {
|
|||
def apply = flagColl.primitiveOne[String]($id("streamer"), "text") dmap (~_) flatMap { text =>
|
||||
val now = org.joda.time.DateTime.now
|
||||
validate(text)._1.map { s =>
|
||||
api.save(Streamer(
|
||||
_id = Id(s.lichessName.toLowerCase),
|
||||
listed = Listed(true),
|
||||
approved = Approved(true),
|
||||
autoFeatured = AutoFeatured(s.featured),
|
||||
chatEnabled = ChatEnabled(s.chat),
|
||||
picturePath = none,
|
||||
name = s.streamerNameForDisplay.map(removeTitle).map(Name.apply),
|
||||
description = none,
|
||||
twitch = s.twitch option Twitch(s.streamerName, Live.empty),
|
||||
youTube = s.youtube option YouTube(s.streamerName, Live.empty),
|
||||
sorting = Sorting.empty,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
))
|
||||
UserRepo named s.lichessName flatMap {
|
||||
_ ?? { user =>
|
||||
api.save(Streamer(
|
||||
_id = Id(s.lichessName.toLowerCase),
|
||||
listed = Listed(true),
|
||||
approved = Approved(true),
|
||||
autoFeatured = AutoFeatured(s.featured),
|
||||
chatEnabled = ChatEnabled(s.chat),
|
||||
picturePath = none,
|
||||
name = Name {
|
||||
s.streamerNameForDisplay.fold(user.realNameOrUsername)(removeTitle)
|
||||
},
|
||||
description = none,
|
||||
twitch = s.twitch option Twitch(s.streamerName, Live.empty),
|
||||
youTube = s.youtube option YouTube(s.streamerName, Live.empty),
|
||||
sorting = Sorting.empty,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
))
|
||||
}
|
||||
}
|
||||
}.sequenceFu.void
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ case class Streamer(
|
|||
autoFeatured: Streamer.AutoFeatured, // on homepage when title contains "lichess.org"
|
||||
chatEnabled: Streamer.ChatEnabled, // embed chat inside lichess
|
||||
picturePath: Option[Streamer.PicturePath],
|
||||
name: Option[Streamer.Name],
|
||||
name: Streamer.Name,
|
||||
description: Option[Streamer.Description],
|
||||
twitch: Option[Streamer.Twitch],
|
||||
youTube: Option[Streamer.YouTube],
|
||||
|
@ -46,7 +46,7 @@ object Streamer {
|
|||
autoFeatured = AutoFeatured(false),
|
||||
chatEnabled = ChatEnabled(true),
|
||||
picturePath = none,
|
||||
name = none,
|
||||
name = Name(user.realNameOrUsername),
|
||||
description = none,
|
||||
twitch = none,
|
||||
youTube = none,
|
||||
|
@ -91,7 +91,7 @@ object Streamer {
|
|||
}
|
||||
object YouTube {
|
||||
private val ChannelIdRegex = """^(\w{11})$""".r
|
||||
private val UrlRegex = """.*(?:youtube\.com|youtu\.be)/(?:watch)?(?:\?v=)?([^"&?\/ ]{11}).*""".r
|
||||
private val UrlRegex = """.*youtube\.com/channel/(\w{11}).*""".r
|
||||
def parseChannelId(str: String): Option[String] = str match {
|
||||
case ChannelIdRegex(c) => c.some
|
||||
case UrlRegex(c) => c.some
|
||||
|
|
|
@ -34,7 +34,7 @@ final class StreamerApi(
|
|||
coll.update($id(s.id), s, upsert = true).void
|
||||
|
||||
def setSeenAt(user: User): Funit =
|
||||
listedIdsCache.get flatMap { ids =>
|
||||
listedIdsCache.get.pp(user.username) flatMap { ids =>
|
||||
ids.contains(Streamer.Id(user.id)) ??
|
||||
coll.update($id(user.id), $set("sorting.seenAt" -> DateTime.now)).void
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ object StreamerForm {
|
|||
import Streamer.{ Name, Description, Twitch, YouTube, Live }
|
||||
|
||||
lazy val emptyUserForm = Form(mapping(
|
||||
"name" -> optional(name),
|
||||
"name" -> name,
|
||||
"description" -> optional(description),
|
||||
"youTube" -> optional(text),
|
||||
"twitch" -> optional(text)
|
||||
"twitch" -> optional(nonEmptyText.verifying("Invalid Twitch username", s => Streamer.Twitch.parseUserId(s).isDefined)),
|
||||
"youTube" -> optional(nonEmptyText.verifying("Invalid YouTube channel", s => Streamer.YouTube.parseChannelId(s).isDefined))
|
||||
)(UserData.apply)(UserData.unapply))
|
||||
|
||||
def userForm(streamer: Streamer) = emptyUserForm fill UserData(
|
||||
|
@ -23,7 +23,7 @@ object StreamerForm {
|
|||
)
|
||||
|
||||
case class UserData(
|
||||
name: Option[Name],
|
||||
name: Name,
|
||||
description: Option[Description],
|
||||
twitch: Option[String],
|
||||
youTube: Option[String]
|
||||
|
|
|
@ -119,15 +119,6 @@ object Trophy {
|
|||
order = 101
|
||||
)
|
||||
|
||||
object Streamer extends Kind(
|
||||
key = "streamer",
|
||||
name = "Lichess streamer",
|
||||
icon = "".some,
|
||||
url = "//lichess.org/help/stream-on-lichess".some,
|
||||
"icon3d".some,
|
||||
order = 102
|
||||
)
|
||||
|
||||
object ZHWC extends Kind(
|
||||
key = "zhwc",
|
||||
name = "Crazyhouse champion",
|
||||
|
@ -138,7 +129,7 @@ object Trophy {
|
|||
)
|
||||
|
||||
val all = List(
|
||||
Streamer, Developer, Moderator,
|
||||
Developer, Moderator,
|
||||
MarathonTopHundred, MarathonTopTen, MarathonTopFifty, MarathonWinner,
|
||||
ZugMiracle, ZHWC,
|
||||
WayOfBerserk,
|
||||
|
|
|
@ -1,21 +1,4 @@
|
|||
$(function() {
|
||||
|
||||
var $editor = $('.streamer_edit');
|
||||
|
||||
var submit = lichess.fp.debounce(function() {
|
||||
$editor.find('form.form').ajaxSubmit({
|
||||
success: function() {
|
||||
$editor.find('div.status').addClass('saved');
|
||||
todo();
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
$editor.find('input, textarea, select')
|
||||
.on("input paste change keyup", function() {
|
||||
$editor.find('div.status').removeClass('saved');
|
||||
submit();
|
||||
});
|
||||
|
||||
$('.streamer_picture form.upload input[type=file]').change(function() {
|
||||
$('.picture_wrap').html(lichess.spinnerHtml);
|
||||
$(this).parents('form').submit();
|
||||
|
|
|
@ -190,7 +190,7 @@ body.dark .material.form .form-group select option {
|
|||
}
|
||||
|
||||
.material.form .has-error .legend.legend,
|
||||
.material.form .has-error .error,
|
||||
body.base .material.form .has-error .error,
|
||||
.has-error.form-group .control-label.control-label {
|
||||
color: #d9534f;
|
||||
}
|
||||
|
|
|
@ -25,23 +25,13 @@
|
|||
margin-top: 80px;
|
||||
}
|
||||
|
||||
.streamer_edit .overview {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.streamer_edit .top a,
|
||||
body.dark .streamer_edit .top a {
|
||||
color: #3893E8;
|
||||
}
|
||||
|
||||
.streamer_edit .material.form {
|
||||
margin-top: 0;
|
||||
}
|
||||
.streamer_edit .material.form .form-group textarea {
|
||||
height: 12em;
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.streamer_picture form {
|
||||
|
@ -79,7 +69,8 @@ body.dark .streamer_edit .top a {
|
|||
font-size: 1.4em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.streamer-new ul li {
|
||||
.streamer-new ul li,
|
||||
.streamer_edit ul li {
|
||||
font-size: 1.2em;
|
||||
margin: 0.5em 0;
|
||||
list-style: disc inside;
|
||||
|
|
|
@ -35,13 +35,17 @@
|
|||
.streamers .streamer .headline {
|
||||
font-family: 'PT Serif', 'Noto Sans', 'Lucida Grande';
|
||||
font-size: 17px;
|
||||
padding: 0 0 20px 0!important;
|
||||
padding: 0 0 10px 0!important;
|
||||
}
|
||||
.streamers .streamer .services {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.streamers .streamer .service {
|
||||
display: flex;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.streamers .streamer .service svg {
|
||||
width: 1.4em;
|
||||
|
|
|
@ -33,7 +33,7 @@ export default function(ctrl: DasherCtrl): VNode {
|
|||
|
||||
!d.streamer ? null : h(
|
||||
'a.text',
|
||||
linkCfg('/streamer/edit', ':'),
|
||||
linkCfg('/streamer/edit', ''),
|
||||
'Streamer manager'),
|
||||
|
||||
h('form', {
|
||||
|
|
Loading…
Reference in New Issue