streamers WIP

streamers
Thibault Duplessis 2017-12-31 00:08:01 -05:00
parent 0fca0bcf99
commit 384a861095
20 changed files with 105 additions and 112 deletions

View File

@ -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,

View File

@ -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())
)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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>

View File

@ -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>
}

View File

@ -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">

View File

@ -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>

View File

@ -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>
}

View File

@ -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>
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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]

View File

@ -119,15 +119,6 @@ object Trophy {
order = 101
)
object Streamer extends Kind(
key = "streamer",
name = "Lichess streamer",
icon = "&#xe003;".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,

View File

@ -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();

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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', {