study chapter manager WIP
parent
c4f9dc5e6a
commit
5c1ddd8f9a
|
@ -16,22 +16,24 @@ object Study extends LilaController {
|
|||
def show(id: String) = Open { implicit ctx =>
|
||||
OptionFuResult(env.api byIdWithChapter id) {
|
||||
case lila.study.Study.WithChapter(study, chapter) =>
|
||||
val setup = chapter.setup
|
||||
val initialFen = chapter.root.fen
|
||||
val pov = UserAnalysis.makePov(initialFen.value.some, setup.variant)
|
||||
Env.round.jsonView.userAnalysisJson(pov, ctx.pref, setup.orientation, owner = false) zip
|
||||
Env.chat.api.userChat.find(study.id) zip
|
||||
env.version(id) map {
|
||||
case ((baseData, chat), sVersion) =>
|
||||
import lila.socket.tree.Node.nodeJsonWriter
|
||||
val analysis = baseData ++ Json.obj(
|
||||
"tree" -> lila.study.TreeBuilder(chapter.root))
|
||||
val data = lila.study.JsonView.JsData(
|
||||
study = lila.study.JsonView.study(study),
|
||||
analysis = analysis,
|
||||
chat = lila.chat.JsonView(chat))
|
||||
Ok(html.study.show(study, data, sVersion))
|
||||
}
|
||||
env.chapterRepo.orderedMetadataByStudy(study.id) flatMap { chapters =>
|
||||
val setup = chapter.setup
|
||||
val initialFen = chapter.root.fen
|
||||
val pov = UserAnalysis.makePov(initialFen.value.some, setup.variant)
|
||||
Env.round.jsonView.userAnalysisJson(pov, ctx.pref, setup.orientation, owner = false) zip
|
||||
Env.chat.api.userChat.find(study.id) zip
|
||||
env.version(id) map {
|
||||
case ((baseData, chat), sVersion) =>
|
||||
import lila.socket.tree.Node.nodeJsonWriter
|
||||
val analysis = baseData ++ Json.obj(
|
||||
"tree" -> lila.study.TreeBuilder(chapter.root))
|
||||
val data = lila.study.JsonView.JsData(
|
||||
study = lila.study.JsonView(study, chapters),
|
||||
analysis = analysis,
|
||||
chat = lila.chat.JsonView(chat))
|
||||
Ok(html.study.show(study, data, sVersion))
|
||||
}
|
||||
}
|
||||
} map NoCache
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ socketVersion: @socketVersion
|
|||
}
|
||||
|
||||
@side = {
|
||||
<div class="side_box">
|
||||
<div class="side_box study_box">
|
||||
@base.spinner()
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ private object BSONHandlers {
|
|||
|
||||
private implicit val ChapterSetupBSONHandler = Macros.handler[Chapter.Setup]
|
||||
implicit val ChapterBSONHandler = Macros.handler[Chapter]
|
||||
implicit val ChapterMetadataBSONHandler = Macros.handler[Chapter.Metadata]
|
||||
|
||||
private implicit val ChaptersMap = MapDocument.MapHandler[Chapter]
|
||||
|
||||
|
|
|
@ -35,6 +35,11 @@ object Chapter {
|
|||
variant: Variant,
|
||||
orientation: Color)
|
||||
|
||||
case class Metadata(
|
||||
_id: Chapter.ID,
|
||||
name: String,
|
||||
setup: Chapter.Setup)
|
||||
|
||||
val idSize = 8
|
||||
|
||||
def makeId = scala.util.Random.alphanumeric take idSize mkString
|
||||
|
|
|
@ -6,12 +6,25 @@ import scala.concurrent.duration._
|
|||
|
||||
import lila.db.dsl._
|
||||
|
||||
private final class ChapterRepo(coll: Coll) {
|
||||
final class ChapterRepo(coll: Coll) {
|
||||
|
||||
import BSONHandlers._
|
||||
|
||||
def byId(id: Chapter.ID): Fu[Option[Chapter]] = coll.byId[Chapter](id)
|
||||
|
||||
def orderedMetadataByStudy(studyId: Study.ID): Fu[List[Chapter.Metadata]] =
|
||||
coll.find(
|
||||
$doc("studyId" -> studyId),
|
||||
$doc("root" -> false)
|
||||
).sort($sort asc "order").list[Chapter.Metadata](64)
|
||||
|
||||
def nextOrderByStudy(studyId: Study.ID): Fu[Int] =
|
||||
coll.primitiveOne[Int](
|
||||
$doc("studyId" -> studyId),
|
||||
$sort desc "order",
|
||||
"order"
|
||||
) map { order => ~order + 1 }
|
||||
|
||||
def exists(id: Chapter.ID) = coll.exists($id(id))
|
||||
|
||||
def insert(s: Chapter): Funit = coll.insert(s).void
|
||||
|
|
|
@ -69,7 +69,7 @@ final class Env(
|
|||
}
|
||||
|
||||
private lazy val studyRepo = new StudyRepo(coll = db(CollectionStudy))
|
||||
private lazy val chapterRepo = new ChapterRepo(coll = db(CollectionChapter))
|
||||
lazy val chapterRepo = new ChapterRepo(coll = db(CollectionChapter))
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
|
|
@ -10,7 +10,8 @@ import lila.socket.Socket.Uid
|
|||
|
||||
object JsonView {
|
||||
|
||||
def study(s: Study) = studyWrites writes s
|
||||
def apply(study: Study, chapters: List[Chapter.Metadata]) =
|
||||
studyWrites.writes(study) ++ Json.obj("chapters" -> chapters)
|
||||
|
||||
private implicit val uciWrites: Writes[Uci] = Writes[Uci] { u =>
|
||||
JsString(u.uci)
|
||||
|
@ -70,6 +71,12 @@ object JsonView {
|
|||
private implicit val variantWrites = Writes[chess.variant.Variant] { v => JsString(v.key) }
|
||||
private implicit val chapterSetupWrites = Json.writes[Chapter.Setup]
|
||||
private[study] implicit val chapterWrites = Json.writes[Chapter]
|
||||
private[study] implicit val chapterMetadataWrites = OWrites[Chapter.Metadata] { c =>
|
||||
Json.obj(
|
||||
"id" -> c._id,
|
||||
"name" -> c.name,
|
||||
"setup" -> c.setup)
|
||||
}
|
||||
|
||||
private implicit val studyWrites = OWrites[Study] { s =>
|
||||
Json.obj(
|
||||
|
|
|
@ -51,6 +51,8 @@ private final class Socket(
|
|||
|
||||
case ReloadMembers(members) => notifyVersion("members", members, Messadata())
|
||||
|
||||
case ReloadChapters(chapters) => notifyVersion("chapters", chapters, Messadata())
|
||||
|
||||
case ReloadShapes(shapes, uid) => notifyVersion("shapes", Json.obj(
|
||||
"s" -> shapes,
|
||||
"w" -> who(uid)
|
||||
|
@ -64,18 +66,16 @@ private final class Socket(
|
|||
|
||||
case ReloadUid(uid) => notifyUid("reload", JsNull)(uid)
|
||||
|
||||
case PingVersion(uid, v) => {
|
||||
case PingVersion(uid, v) =>
|
||||
ping(uid)
|
||||
timeBomb.delay
|
||||
withMember(uid) { m =>
|
||||
history.since(v).fold(resync(m))(_ foreach sendMessage(m))
|
||||
}
|
||||
}
|
||||
|
||||
case Broom => {
|
||||
case Broom =>
|
||||
broom
|
||||
if (timeBomb.boom) self ! PoisonPill
|
||||
}
|
||||
|
||||
case GetVersion => sender ! history.version
|
||||
|
||||
|
@ -129,6 +129,7 @@ private object Socket {
|
|||
case class SetPath(position: Position.Ref, uid: Uid)
|
||||
case class ReloadMembers(members: StudyMembers)
|
||||
case class ReloadShapes(shapes: List[Shape], uid: Uid)
|
||||
case class ReloadChapters(chapters: List[Chapter.Metadata])
|
||||
|
||||
case class Messadata(trollish: Boolean = false)
|
||||
case object NotifyCrowd
|
||||
|
|
|
@ -120,6 +120,13 @@ private[study] final class SocketHandler(
|
|||
api.setShapes(userId, studyId, shapes, uid)
|
||||
}
|
||||
}
|
||||
|
||||
case ("addChapter", o) if owner => for {
|
||||
byUserId <- member.userId
|
||||
d <- o obj "d"
|
||||
name <- d str "name"
|
||||
} api.addChapter(byUserId, studyId, name)
|
||||
|
||||
}
|
||||
|
||||
private def reading[A](o: JsValue)(f: A => Unit)(implicit reader: Reads[A]): Unit =
|
||||
|
|
|
@ -62,7 +62,7 @@ final class StudyApi(
|
|||
def addNode(studyId: Study.ID, position: Position.Ref, node: Node, uid: Uid) = sequenceStudyWithChapter(studyId) {
|
||||
case Study.WithChapter(study, chapter) => Contribute(node.by, study) {
|
||||
chapter.addNode(position.path, node) match {
|
||||
case None => funit >>- reloadUid(study, uid)
|
||||
case None => fufail(s"Invalid addNode $position $node") >>- reloadUid(study, uid)
|
||||
case Some(newChapter) =>
|
||||
chapterRepo.update(newChapter) >>
|
||||
studyRepo.setPosition(study.id, position + node) >>-
|
||||
|
@ -121,6 +121,20 @@ final class StudyApi(
|
|||
} >>- reloadShapes(study, uid)
|
||||
}
|
||||
|
||||
def addChapter(byUserId: User.ID, studyId: Study.ID, name: String) = sequenceStudy(studyId) { study =>
|
||||
(study isOwner byUserId) ?? {
|
||||
chapterRepo.nextOrderByStudy(study.id) flatMap { order =>
|
||||
val chapter = Chapter.make(
|
||||
studyId = study.id,
|
||||
name = name,
|
||||
setup = Chapter.Setup(none, chess.variant.Standard, chess.White),
|
||||
root = Node.Root.default,
|
||||
order = order)
|
||||
chapterRepo.insert(chapter) >>- reloadChapters(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def reloadUid(study: Study, uid: Uid) =
|
||||
sendTo(study.id, Socket.ReloadUid(uid))
|
||||
|
||||
|
@ -131,6 +145,11 @@ final class StudyApi(
|
|||
}
|
||||
}
|
||||
|
||||
private def reloadChapters(study: Study) =
|
||||
chapterRepo.orderedMetadataByStudy(study.id).foreach { chapters =>
|
||||
sendTo(study.id, Socket.ReloadChapters(chapters))
|
||||
}
|
||||
|
||||
private def reloadShapes(study: Study, uid: Uid) =
|
||||
studyRepo.getShapes(study.id).foreach { shapes =>
|
||||
sendTo(study.id, Socket.ReloadShapes(shapes, uid))
|
||||
|
|
|
@ -1,23 +1,43 @@
|
|||
.members .member {
|
||||
.study_box .spinner {
|
||||
margin: 20px auto;
|
||||
}
|
||||
.study_box .tabs {
|
||||
display: flex;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.study_box .tabs a {
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
background: #fff;
|
||||
}
|
||||
.study_box .tabs a.active {
|
||||
border-bottom: 3px solid #3893E8;
|
||||
}
|
||||
|
||||
.study_box .list .elem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.member .user_link {
|
||||
.study_box .elem .user_link {
|
||||
font-size: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 16px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.members .left,
|
||||
.members .right {
|
||||
.study_box .list .left,
|
||||
.study_box .list .right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.members .status,
|
||||
.members .action {
|
||||
.study_box .list .status,
|
||||
.study_box .list .action {
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
@ -25,74 +45,75 @@
|
|||
text-align: center;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.members .status i,
|
||||
.members .action i {
|
||||
.study_box .list .status i,
|
||||
.study_box .list .action i {
|
||||
font-size: 14px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.13s;
|
||||
}
|
||||
.members .action {
|
||||
.study_box .list .action {
|
||||
cursor: pointer;
|
||||
}
|
||||
.members .action.follow i::before {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.members > div:hover .action i {
|
||||
.study_box .list > div:hover .action i {
|
||||
opacity: 1;
|
||||
}
|
||||
.members .status.contrib i {
|
||||
.study_box .members .status.contrib i {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.members .status {
|
||||
.study_box .members .status {
|
||||
transition: 3s;
|
||||
}
|
||||
.members .status i {
|
||||
.study_box .members .status i {
|
||||
transition: 3s;
|
||||
}
|
||||
.members .status.active {
|
||||
.study_box .members .status.active {
|
||||
transition: none;
|
||||
}
|
||||
.members .member:nth-child(4n-3) .status.active {
|
||||
.study_box .members .member:nth-child(4n-3) .status.active {
|
||||
background: #42a5f5;
|
||||
}
|
||||
.members .member:nth-child(4n-2) .status.active {
|
||||
.study_box .members .member:nth-child(4n-2) .status.active {
|
||||
background: #f44336;
|
||||
}
|
||||
.members .member:nth-child(4n-1) .status.active {
|
||||
.study_box .members .member:nth-child(4n-1) .status.active {
|
||||
background: #fdd835;
|
||||
}
|
||||
.members .member:nth-child(4n-0) .status.active {
|
||||
.study_box .members .member:nth-child(4n-0) .status.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
.members .status.active i {
|
||||
.study_box .members .status.active i {
|
||||
transition: none;
|
||||
opacity: 1;
|
||||
color: #fff;
|
||||
}
|
||||
.members .invite input {
|
||||
.study_box .list_input {
|
||||
width: 214px;
|
||||
border: none;
|
||||
border-top: 1px solid #ccc;
|
||||
background: white;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.members div.confing,
|
||||
.members div.config {
|
||||
.study_box .list div.confing,
|
||||
.study_box .list div.config {
|
||||
background: #fff;
|
||||
}
|
||||
.members div.config {
|
||||
.study_box .list div.config {
|
||||
padding: 5px 10px 15px 10px;
|
||||
}
|
||||
.members div.config .role {
|
||||
.study_box .list div.config .role {
|
||||
line-height: 22px;
|
||||
}
|
||||
.members div.config .role label {
|
||||
.study_box .list div.config .role label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.members div.config .switch {
|
||||
.study_box .list div.config .switch {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.members div.config .kick {
|
||||
.study_box .list div.config .kick {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.study_box .chapters .status i {
|
||||
color: #3893E8;
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
var m = require('mithril');
|
||||
var classSet = require('chessground').util.classSet;
|
||||
|
||||
module.exports = {
|
||||
ctrl: function(chapters, send) {
|
||||
|
||||
var vm = {
|
||||
confing: null // which chapter is being configured by us
|
||||
};
|
||||
|
||||
return {
|
||||
vm: vm,
|
||||
list: function() {
|
||||
return chapters;
|
||||
},
|
||||
set: function(cs) {
|
||||
chapters = cs;
|
||||
},
|
||||
add: function(name) {
|
||||
send("addChapter", {
|
||||
name: name
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
view: function(ctrl) {
|
||||
|
||||
var ownage = ctrl.members.isOwner();
|
||||
|
||||
var configButton = function(chapter, confing) {
|
||||
if (ownage) return m('span.action.config', {
|
||||
onclick: function(e) {
|
||||
ctrl.chapters.vm.confing = confing ? null : chapter.id;
|
||||
}
|
||||
}, m('i', {
|
||||
'data-icon': '%'
|
||||
}));
|
||||
};
|
||||
|
||||
var chapterConfig = function(chapter) {
|
||||
return m('div.config', [
|
||||
"config"
|
||||
]);
|
||||
};
|
||||
|
||||
var create = function() {
|
||||
return m('div.create', [
|
||||
m('input', {
|
||||
class: 'list_input',
|
||||
config: function(el, isUpdate) {
|
||||
if (isUpdate) return;
|
||||
$(el).keypress(function(e) {
|
||||
if (e.which == 10 || e.which == 13) ctrl.chapters.add($(this).val());
|
||||
})
|
||||
},
|
||||
placeholder: 'Add a new chapter'
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
||||
return m('div', {
|
||||
class: 'list chapters' + (ownage ? ' ownage' : '')
|
||||
}, [
|
||||
ctrl.chapters.list().map(function(chapter) {
|
||||
var confing = ctrl.chapters.vm.confing === chapter.id;
|
||||
var active = ctrl.position().chapterId === chapter.id;
|
||||
var attrs = {
|
||||
class: classSet({
|
||||
elem: true,
|
||||
chapter: true,
|
||||
active: active,
|
||||
confing: confing
|
||||
})
|
||||
};
|
||||
return [
|
||||
m('div', attrs, [
|
||||
m('div.left', [
|
||||
m('span.status', m('i', {
|
||||
'data-icon': active ? 'J' : 'K'
|
||||
})),
|
||||
chapter.name
|
||||
]),
|
||||
m('div.right', [
|
||||
configButton(chapter, confing)
|
||||
])
|
||||
]),
|
||||
confing ? chapterConfig(chapter) : null
|
||||
];
|
||||
}),
|
||||
ownage ? create() : null
|
||||
]);
|
||||
}
|
||||
};
|
|
@ -1,7 +1,9 @@
|
|||
var m = require('mithril');
|
||||
var partial = require('chessground').util.partial;
|
||||
var throttle = require('../util').throttle;
|
||||
var storedProp = require('../util').storedProp;
|
||||
var memberCtrl = require('./studyMembers').ctrl;
|
||||
var chapterCtrl = require('./studyChapters').ctrl;
|
||||
|
||||
module.exports = {
|
||||
// data.position.path represents the server state
|
||||
|
@ -10,10 +12,15 @@ module.exports = {
|
|||
|
||||
var send = ctrl.socket.send;
|
||||
|
||||
var members = memberCtrl(data.members, ctrl.userId, data.ownerId);
|
||||
var members = memberCtrl(data.members, ctrl.userId, data.ownerId, send);
|
||||
var chapters = chapterCtrl(data.chapters, send);
|
||||
|
||||
var sri = lichess.StrongSocket.sri;
|
||||
|
||||
var vm = {
|
||||
tab: storedProp('study.tab', 'members')
|
||||
};
|
||||
|
||||
function addChapterId(req) {
|
||||
req.chapterId = data.position.chapterId;
|
||||
return req;
|
||||
|
@ -41,6 +48,8 @@ module.exports = {
|
|||
return {
|
||||
data: data,
|
||||
members: members,
|
||||
chapters: chapters,
|
||||
vm: vm,
|
||||
position: function() {
|
||||
return data.position;
|
||||
},
|
||||
|
@ -62,23 +71,6 @@ module.exports = {
|
|||
path: path
|
||||
}));
|
||||
},
|
||||
setRole: function(id, role) {
|
||||
send("setRole", {
|
||||
userId: id,
|
||||
role: role
|
||||
});
|
||||
setTimeout(function() {
|
||||
members.vm.confing = null;
|
||||
m.redraw();
|
||||
}, 400);
|
||||
},
|
||||
invite: function(username) {
|
||||
send("invite", username);
|
||||
},
|
||||
kick: function(id) {
|
||||
send("kick", id);
|
||||
vm.memberConfig = null;
|
||||
},
|
||||
onShowGround: function() {
|
||||
updateShapes();
|
||||
},
|
||||
|
@ -132,6 +124,10 @@ module.exports = {
|
|||
members.set(d);
|
||||
m.redraw();
|
||||
},
|
||||
chapters: function(d) {
|
||||
chapters.set(d);
|
||||
m.redraw();
|
||||
},
|
||||
shapes: function(d) {
|
||||
members.setActive(d.w.u);
|
||||
data.shapes = d.s;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
var m = require('mithril');
|
||||
var classSet = require('chessground').util.classSet;
|
||||
|
||||
function memberActivity(onIdle) {
|
||||
var timeout;
|
||||
|
@ -16,7 +17,7 @@ function memberActivity(onIdle) {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
ctrl: function(members, myId, ownerId) {
|
||||
ctrl: function(members, myId, ownerId, send) {
|
||||
|
||||
var vm = {
|
||||
confing: null // which user is being configured by us
|
||||
|
@ -55,6 +56,23 @@ module.exports = {
|
|||
canContribute: function() {
|
||||
return myMember() && myMember().role === 'w';
|
||||
},
|
||||
setRole: function(id, role) {
|
||||
send("setRole", {
|
||||
userId: id,
|
||||
role: role
|
||||
});
|
||||
setTimeout(function() {
|
||||
vm.confing = null;
|
||||
m.redraw();
|
||||
}, 400);
|
||||
},
|
||||
invite: function(username) {
|
||||
send("invite", username);
|
||||
},
|
||||
kick: function(id) {
|
||||
send("kick", id);
|
||||
vm.confing = null;
|
||||
},
|
||||
ordered: function() {
|
||||
return Object.keys(members).map(function(id) {
|
||||
return members[id];
|
||||
|
@ -63,5 +81,119 @@ module.exports = {
|
|||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
view: function(ctrl) {
|
||||
|
||||
var ownage = ctrl.members.isOwner();
|
||||
|
||||
var username = function(member) {
|
||||
var u = member.user;
|
||||
return m('span.user_link.ulpt', {
|
||||
'data-href': '/@/' + u.name
|
||||
}, (u.title ? u.title + ' ' : '') + u.name);
|
||||
};
|
||||
|
||||
var statusIcon = function(member) {
|
||||
var contrib = member.role === 'w';
|
||||
return m('span', {
|
||||
class: classSet({
|
||||
contrib: contrib,
|
||||
active: ctrl.members.isActive(member.user.id),
|
||||
status: true,
|
||||
'hint--top': true
|
||||
}),
|
||||
'data-hint': contrib ? 'Contributor' : 'Viewer',
|
||||
}, m('i', {
|
||||
'data-icon': contrib ? '' : 'v'
|
||||
}));
|
||||
};
|
||||
|
||||
var configButton = function(member, confing) {
|
||||
if (!ownage || member.user.id === ctrl.members.myId) return null;
|
||||
return m('span.action.config', {
|
||||
onclick: function(e) {
|
||||
ctrl.members.vm.confing = confing ? null : member.user.id;
|
||||
}
|
||||
}, m('i', {
|
||||
'data-icon': '%'
|
||||
}));
|
||||
};
|
||||
|
||||
var invite = function() {
|
||||
return m('div.invite', [
|
||||
m('input', {
|
||||
class: 'list_input',
|
||||
config: function(el, isUpdate) {
|
||||
if (isUpdate) return;
|
||||
lichess.userAutocomplete($(el), {
|
||||
onSelect: function(v) {
|
||||
ctrl.members.invite(v);
|
||||
}
|
||||
});
|
||||
},
|
||||
placeholder: 'Invite someone'
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
||||
var memberConfig = function(member) {
|
||||
return m('div.config', [
|
||||
(function(id) {
|
||||
return m('div.role', [
|
||||
m('div.switch', [
|
||||
m('input', {
|
||||
id: id,
|
||||
class: 'cmn-toggle cmn-toggle-round',
|
||||
type: 'checkbox',
|
||||
checked: member.role === 'w',
|
||||
onchange: function(e) {
|
||||
ctrl.members.setRole(member.user.id, e.target.checked ? 'w' : 'r');
|
||||
}
|
||||
}),
|
||||
m('label', {
|
||||
'for': id
|
||||
})
|
||||
]),
|
||||
m('label', {
|
||||
'for': id
|
||||
}, 'Contributor')
|
||||
]);
|
||||
})('member-role'),
|
||||
m('div.kick', m('a.button.text[data-icon=L]', {
|
||||
onclick: function() {
|
||||
if (confirm('Kick ' + member.user.name + ' out of the study?'))
|
||||
ctrl.members.kick(member.user.id);
|
||||
}
|
||||
}, 'Kick from this study'))
|
||||
]);
|
||||
};
|
||||
|
||||
return m('div', {
|
||||
class: 'list members' + (ownage ? ' ownage' : '')
|
||||
}, [
|
||||
ctrl.members.ordered().map(function(member) {
|
||||
var confing = ctrl.members.vm.confing === member.user.id;
|
||||
var attrs = {
|
||||
class: classSet({
|
||||
elem: true,
|
||||
member: true,
|
||||
confing: confing
|
||||
})
|
||||
};
|
||||
return [
|
||||
m('div', attrs, [
|
||||
m('div.left', [
|
||||
statusIcon(member),
|
||||
username(member)
|
||||
]),
|
||||
m('div.right', [
|
||||
configButton(member, confing)
|
||||
])
|
||||
]),
|
||||
confing ? memberConfig(member) : null
|
||||
];
|
||||
}),
|
||||
ownage ? invite() : null
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,125 +1,30 @@
|
|||
var m = require('mithril');
|
||||
var partial = require('chessground').util.partial;
|
||||
var classSet = require('chessground').util.classSet;
|
||||
var memberView = require('./studyMembers').view;
|
||||
var chapterView = require('./studyChapters').view;
|
||||
|
||||
module.exports = function(ctrl) {
|
||||
|
||||
var ownage = ctrl.members.isOwner();
|
||||
|
||||
var username = function(member) {
|
||||
var u = member.user;
|
||||
return m('span.user_link.ulpt', {
|
||||
'data-href': '/@/' + u.name
|
||||
}, (u.title ? u.title + ' ' : '') + u.name);
|
||||
var makeTab = function(key, name) {
|
||||
return m('a', {
|
||||
class: ctrl.vm.tab() === key ? 'active' : '',
|
||||
onclick: partial(ctrl.vm.tab, key),
|
||||
}, name);
|
||||
};
|
||||
|
||||
var roleToggle = function(member) {
|
||||
return m('span.role.hint--top', {
|
||||
'data-hint': member.role === 'r' ? 'Can read' : 'Can write',
|
||||
onclick: ownage ? function() {
|
||||
ctrl.toggleRole(member.user.id);
|
||||
} : $.noop
|
||||
}, member.role.toUpperCase());
|
||||
};
|
||||
var tabs = m('div.tabs', [
|
||||
makeTab('members', 'Members'),
|
||||
makeTab('chapters', 'Chapters'),
|
||||
makeTab('settings', 'Settings')
|
||||
]);
|
||||
|
||||
var statusIcon = function(member) {
|
||||
var contrib = member.role === 'w';
|
||||
return m('span', {
|
||||
class: classSet({
|
||||
contrib: contrib,
|
||||
active: ctrl.members.isActive(member.user.id),
|
||||
status: true,
|
||||
'hint--top': true
|
||||
}),
|
||||
'data-hint': contrib ? 'Contributor' : 'Viewer',
|
||||
}, m('i', {
|
||||
'data-icon': contrib ? '' : 'v'
|
||||
}));
|
||||
};
|
||||
var panel;
|
||||
if (ctrl.vm.tab() === 'members') panel = memberView(ctrl);
|
||||
else if (ctrl.vm.tab() === 'chapters') panel = chapterView(ctrl);
|
||||
|
||||
var configButton = function(member, confing) {
|
||||
if (!ownage || member.user.id === ctrl.members.myId) return null;
|
||||
return m('span.action.config', {
|
||||
onclick: function(e) {
|
||||
ctrl.members.vm.confing = confing ? null : member.user.id;
|
||||
}
|
||||
}, m('i', {
|
||||
'data-icon': '%'
|
||||
}));
|
||||
};
|
||||
|
||||
var invite = function() {
|
||||
return m('div.invite', [
|
||||
m('input', {
|
||||
config: function(el, isUpdate) {
|
||||
if (isUpdate) return;
|
||||
lichess.userAutocomplete($(el), {
|
||||
onSelect: function(v) {
|
||||
ctrl.invite(v);
|
||||
}
|
||||
});
|
||||
},
|
||||
class: 'add_member',
|
||||
placeholder: 'Invite someone'
|
||||
})
|
||||
]);
|
||||
};
|
||||
|
||||
var memberConfig = function(member) {
|
||||
return m('div.config', [
|
||||
(function(id) {
|
||||
return m('div.role', [
|
||||
m('div.switch', [
|
||||
m('input', {
|
||||
id: id,
|
||||
class: 'cmn-toggle cmn-toggle-round',
|
||||
type: 'checkbox',
|
||||
checked: member.role === 'w',
|
||||
onchange: function(e) {
|
||||
ctrl.setRole(member.user.id, e.target.checked ? 'w' : 'r');
|
||||
}
|
||||
}),
|
||||
m('label', {
|
||||
'for': id
|
||||
})
|
||||
]),
|
||||
m('label', {
|
||||
'for': id
|
||||
}, 'Contributor')
|
||||
]);
|
||||
})('member-role'),
|
||||
m('div.kick', m('a.button.text[data-icon=L]', {
|
||||
onclick: function() {
|
||||
if (confirm('Kick ' + member.user.name + ' out of the study?'))
|
||||
ctrl.kick(member.user.id);
|
||||
}
|
||||
}, 'Kick from this study'))
|
||||
]);
|
||||
};
|
||||
|
||||
return m('div', {
|
||||
class: 'members' + (ownage ? ' ownage' : '')
|
||||
},
|
||||
ctrl.members.ordered().map(function(member) {
|
||||
var confing = ctrl.members.vm.confing === member.user.id;
|
||||
var attrs = {
|
||||
class: classSet({
|
||||
member: true,
|
||||
confing: confing
|
||||
})
|
||||
};
|
||||
return [
|
||||
m('div', attrs, [
|
||||
m('div.left', [
|
||||
statusIcon(member),
|
||||
username(member)
|
||||
]),
|
||||
m('div.right', [
|
||||
configButton(member, confing)
|
||||
])
|
||||
]),
|
||||
confing ? memberConfig(member) : null
|
||||
];
|
||||
}),
|
||||
ownage ? invite() : null);
|
||||
return [
|
||||
tabs,
|
||||
panel
|
||||
];
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue