diff options
Diffstat (limited to 'client/components')
76 files changed, 5922 insertions, 0 deletions
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade new file mode 100644 index 00000000..1c6b9faf --- /dev/null +++ b/client/components/activities/activities.jade @@ -0,0 +1,8 @@ +template(name="activities") + .js-sidebar-activities + //- We should use Template.dynamic here but there is a bug with + //- blaze-components: https://github.com/peerlibrary/meteor-blaze-components/issues/30 + if $eq mode "board" + +boardActivities + else + +cardActivities diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js new file mode 100644 index 00000000..c806e87b --- /dev/null +++ b/client/components/activities/activities.js @@ -0,0 +1,77 @@ +var activitiesPerPage = 20; + +BlazeComponent.extendComponent({ + template: function() { + return 'activities'; + }, + + onCreated: function() { + var self = this; + // XXX Should we use ReactiveNumber? + self.page = new ReactiveVar(1); + self.loadNextPageLocked = false; + var sidebar = self.componentParent(); // XXX for some reason not working + sidebar.callFirstWith(null, 'resetNextPeak'); + self.autorun(function() { + var mode = self.data().mode; + var capitalizedMode = Utils.capitalize(mode); + var id = Session.get('current' + capitalizedMode); + var limit = self.page.get() * activitiesPerPage; + if (id === null) + return; + + self.subscribe('activities', mode, id, limit, function() { + self.loadNextPageLocked = false; + + // If the sibear peak hasn't increased, that mean that there are no more + // activities, and we can stop calling new subscriptions. + // XXX This is hacky! We need to know excatly and reactively how many + // activities there are, we probably want to denormalize this number + // dirrectly into card and board documents. + var a = sidebar.callFirstWith(null, 'getNextPeak'); + sidebar.calculateNextPeak(); + var b = sidebar.callFirstWith(null, 'getNextPeak'); + if (a === b) { + sidebar.callFirstWith(null, 'resetNextPeak'); + } + }); + }); + }, + + loadNextPage: function() { + if (this.loadNextPageLocked === false) { + this.page.set(this.page.get() + 1); + this.loadNextPageLocked = true; + } + }, + + boardLabel: function() { + return TAPi18n.__('this-board'); + }, + + cardLabel: function() { + return TAPi18n.__('this-card'); + }, + + cardLink: function() { + var card = this.currentData().card(); + return Blaze.toHTML(HTML.A({ + href: card.absoluteUrl(), + 'class': 'action-card' + }, card.title)); + }, + + memberLink: function() { + return Blaze.toHTMLWithData(Template.memberName, { + user: this.currentData().member() + }); + }, + + attachmentLink: function() { + var attachment = this.currentData().attachment(); + return Blaze.toHTML(HTML.A({ + href: attachment.url(), + 'class': 'js-open-attachment-viewer' + }, attachment.name())); + } +}).register('activities'); diff --git a/client/components/activities/comments.jade b/client/components/activities/comments.jade new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/client/components/activities/comments.jade diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/client/components/activities/comments.js diff --git a/client/components/activities/events.js b/client/components/activities/events.js new file mode 100644 index 00000000..ea98e65f --- /dev/null +++ b/client/components/activities/events.js @@ -0,0 +1,30 @@ +Template.cardActivities.events({ + 'click .js-edit-action': function(evt) { + var $this = $(evt.currentTarget); + var container = $this.parents('.phenom-comment'); + + // open and focus + container.addClass('editing'); + container.find('textarea').focus(); + }, + 'click .js-confirm-delete-action': function() { + CardComments.remove(this._id); + }, + 'submit form': function(evt) { + var $this = $(evt.currentTarget); + var container = $this.parents('.phenom-comment'); + var text = container.find('textarea'); + + if ($.trim(text.val())) { + CardComments.update(this._id, { + $set: { + text: text.val() + } + }); + + // reset editing class + $('.editing').removeClass('editing'); + } + evt.preventDefault(); + } +}); diff --git a/client/components/activities/templates.html b/client/components/activities/templates.html new file mode 100644 index 00000000..8d3ff763 --- /dev/null +++ b/client/components/activities/templates.html @@ -0,0 +1,154 @@ +<template name="boardActivities"> + {{# each currentBoard.activities }} + <div class="phenom phenom-action clearfix phenom-other"> + {{> userAvatar user=user size="extra-small" class="creator js-show-mem-menu" }} + <div class="phenom-desc"> + {{ > memberName user=user }} + + {{# if $eq activityType 'createBoard' }} + {{_ 'activity-created' boardLabel}}. + {{ /if }} + + {{# if $eq activityType 'createList' }} + {{_ 'activity-added' list.title boardLabel}}. + {{ /if }} + + {{# if $eq activityType 'archivedList' }} + {{_ 'activity-archived' list.title}}. + {{ /if }} + + {{# if $eq activityType 'createCard' }} + {{{_ 'activity-added' cardLink boardLabel}}}. + {{ /if }} + + {{# if $eq activityType 'archivedCard' }} + {{{_ 'activity-archived' cardLink}}}. + {{ /if }} + + {{# if $eq activityType 'restoredCard' }} + {{{_ 'activity-sent' cardLink boardLabel}}}. + {{ /if }} + + {{# if $eq activityType 'moveCard' }} + {{{_ 'activity-moved' cardLink oldList.title list.title}}}. + {{ /if }} + + {{# if $eq activityType 'addBoardMember' }} + {{{_ 'activity-added' memberLink boardLabel}}}. + {{ /if }} + + {{# if $eq activityType 'removeBoardMember' }} + {{{_ 'activity-excluded' memberLink boardLabel}}}. + {{ /if }} + + {{# if $eq activityType 'joinMember' }} + {{# if $eq currentUser._id member._id }} + {{{_ 'activity-joined' cardLink}}}. + {{ else }} + {{{_ 'activity-added' memberLink cardLink}}}. + {{/if}} + {{ /if }} + + {{# if $eq activityType 'unjoinMember' }} + {{# if $eq currentUser._id member._id }} + {{{_ 'activity-unjoined' cardLink}}}. + {{ else }} + {{{_ 'activity-removed' memberLink cardLink}}}. + {{/if}} + {{ /if }} + + {{# if $eq activityType 'addComment' }} + <div class="phenom-desc"> + {{{_ 'activity-on' cardLink}}} + <div class="action-comment markeddown"> + <a href="{{ card.absoluteUrl }}" class="current-comment show tdn"> + <p>{{#viewer}}{{ comment.text }}{{/viewer}}</p> + </a> + </div> + </div> + {{ /if }} + + {{# if $eq activityType 'addAttachment' }} + <div class="phenom-desc"> + {{{_ 'activity-attached' attachmentLink cardLink}}}. + </div> + {{ /if }} + </div> + <p class="phenom-meta quiet"> + <span class="date js-hide-on-sending"> + {{ moment createdAt }} + </span> + </p> + </div> + {{ /each }} +</template> + +<template name="cardActivities"> + {{# each currentCard.comments }} + <div class="phenom phenom-action clearfix phenom-comment"> + {{> userAvatar user=user size="small" class="creator js-show-mem-menu" }} + <form> + <div class="phenom-desc"> + {{ > memberName user=user }} + <div class="action-comment markeddown"> + <div class="current-comment"> + {{#viewer}}{{ text }}{{/viewer}} + </div> + <textarea class="js-text" tabindex="1">{{ text }}</textarea> + </div> + </div> + <div class="edit-controls clearfix"> + <input type="submit" class="primary confirm js-save-edit" value="{{_ 'save'}}" tabindex="2"> + </div> + </form> + <p class="phenom-meta quiet"> + <span class="date js-hide-on-sending">{{ moment createdAt }}</span> + {{# if currentUser }} + <span class="js-hide-on-sending"> + - <a href="#" class="js-edit-action">{{_ "edit"}}</a> + - <a href="#" class="js-confirm-delete-action">{{_ "delete"}}</a> + </span> + {{/ if }} + </p> + </div> + {{/each}} + + {{# each currentCard.activities }} + <div class="phenom phenom-action clearfix phenom-other"> + {{> userAvatar user=user size="extra-small" class="creator js-show-mem-menu" }} + {{ > memberName user=user }} + {{# if $eq activityType 'createCard' }} + {{_ 'activity-added' cardLabel list.title}}. + {{ /if }} + {{# if $eq activityType 'joinMember' }} + {{# if $eq currentUser._id member._id }} + {{_ 'activity-joined' cardLabel}}. + {{ else }} + {{{_ 'activity-added' cardLabel memberLink}}}. + {{/if}} + {{/if}} + {{# if $eq activityType 'unjoinMember' }} + {{# if $eq currentUser._id member._id }} + {{_ 'activity-unjoined' cardLabel}}. + {{ else }} + {{{_ 'activity-removed' cardLabel memberLink}}}. + {{/if}} + {{ /if }} + {{# if $eq activityType 'archivedCard' }} + {{_ 'activity-archived' cardLabel}}. + {{ /if }} + {{# if $eq activityType 'restoredCard' }} + {{_ 'activity-sent' cardLabel boardLabel}}. + {{/ if }} + {{# if $eq activityType 'moveCard' }} + {{_ 'activity-moved' cardLabel oldList.title list.title}}. + {{/ if }} + {{# if $eq activityType 'addAttachment' }} + {{{_ 'activity-attached' attachmentLink cardLabel}}}. + {{# if attachment.isImage }} + <img src="{{ attachment.url }}" class="attachment-image-preview"> + {{/if}} + {{/ if}} + </div> + {{/each}} +</template> diff --git a/client/components/boards/body.jade b/client/components/boards/body.jade new file mode 100644 index 00000000..5406ee2f --- /dev/null +++ b/client/components/boards/body.jade @@ -0,0 +1,33 @@ +//- + XXX This template can't be transformed into a component because it is + included by iron-router. That's a bug. +template(name="board") + +boardComponent + +template(name="boardComponent") + if this + .board-wrapper(class=colorClass) + .board-canvas(class=sidebarSize) + .lists.js-lists + each lists + +list(this) + if currentUser.isBoardMember + +addlistForm + +boardSidebar + if currentCard + +cardSidebar(currentCard) + else + +message(label="board-no-found") + +template(name="addlistForm") + .list.js-list.add-list.js-add-list + +inlinedForm(autoclose=false) + input.list-name-input(type="text" placeholder="{{_ 'add-list'}}" + autocomplete="off" autofocus) + div.edit-controls.clearfix + button.primary.confirm.js-save-edit(type="submit") {{_ 'save'}} + a.fa.fa-times.dark-hover.cancel.js-close-inlined-form + else + .js-open-inlined-form + i.fa.fa-plus + | {{_ 'add-list'}} diff --git a/client/components/boards/body.js b/client/components/boards/body.js new file mode 100644 index 00000000..2b4baf53 --- /dev/null +++ b/client/components/boards/body.js @@ -0,0 +1,70 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'boardComponent'; + }, + + openNewListForm: function() { + this.componentChildren('addlistForm')[0].open(); + }, + + scrollLeft: function() { + // TODO + }, + + onRendered: function() { + var self = this; + + self.scrollLeft(); + + if (Meteor.user().isBoardMember()) { + self.$('.js-lists').sortable({ + tolerance: 'pointer', + appendTo: '.js-lists', + helper: 'clone', + items: '.js-list:not(.add-list)', + placeholder: 'list placeholder', + start: function(event, ui) { + $('.list.placeholder').height(ui.item.height()); + Popup.close(); + }, + stop: function() { + self.$('.js-lists').find('.js-list:not(.add-list)').each( + function(i, list) { + var data = Blaze.getData(list); + Lists.update(data._id, { + $set: { + sort: i + } + }); + } + ); + } + }); + + // If there is no data in the board (ie, no lists) we autofocus the list + // creation form by clicking on the corresponding element. + if (self.data().lists().count() === 0) { + this.openNewListForm(); + } + } + }, + + sidebarSize: function() { + var sidebar = this.componentChildren('boardSidebar')[0]; + if (Session.get('currentCard') !== null) + return 'next-large-sidebar'; + else if (sidebar && sidebar.isOpen()) + return 'next-small-sidebar'; + } +}).register('boardComponent'); + +BlazeComponent.extendComponent({ + template: function() { + return 'addlistForm'; + }, + + // Proxy + open: function() { + this.componentChildren('inlinedForm')[0].open(); + } +}).register('addlistForm'); diff --git a/client/components/boards/body.styl b/client/components/boards/body.styl new file mode 100644 index 00000000..cb351e46 --- /dev/null +++ b/client/components/boards/body.styl @@ -0,0 +1,54 @@ +@import 'nib' + +.board-wrapper + left: 0 + top: 0 + bottom: 0 + right: 0 + position: absolute + overflow: hidden + + .board-canvas + position: absolute + left: 0 + right: 0 + top: 0 + bottom: 0 + transition: margin .1s + + &.next-small-sidebar + margin-right: 248px + + &.next-large-sidebar + opacity: 0.8 + margin-right: 496px + +.lists + align-items: flex-start + display: flex + flex-direction: row + margin-bottom: 10px + overflow-x: auto + overflow-y: hidden + padding-bottom: 10px + position: absolute + top: 0 + right: 0 + bottom: 0 + left: 0 + + &::-webkit-scrollbar + height: 13px + width: 13px + + &::-webkit-scrollbar-thumb:vertical, + &::-webkit-scrollbar-thumb:horizontal + background: rgba(255, 255, 255, .4) + + &::-webkit-scrollbar-track-piece + background: rgba(0, 0, 0, .15) + + &::-webkit-scrollbar-button + display: block + height: 5px + width: 5px diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl new file mode 100644 index 00000000..1db44845 --- /dev/null +++ b/client/components/boards/colors.styl @@ -0,0 +1,34 @@ +// We define a set of six board colors that we took from the FlatUI palette. +// http://flatuicolors.com + +setBoardColor(color) + &#header, + &.sk-spinner div, + .board-backgrounds-list &.background-box, + &.pop-over .pop-over-list li a:hover, + .board-list & a + background-color: color + + & .minicard.is-selected .minicard-details + border-bottom: 2px solid color + + button[type=submit].primary, input[type=submit].primary + background-color: darken(color, 20%) + +.board-color-nephritis + setBoardColor(#27AE60) + +.board-color-pomegranate + setBoardColor(#C0392B) + +.board-color-belize + setBoardColor(#2980B9) + +.board-color-wisteria + setBoardColor(#8E44AD) + +.board-color-midnight + setBoardColor(#2C3E50) + +.board-color-pumpkin + setBoardColor(#E67E22) diff --git a/client/components/boards/events.js b/client/components/boards/events.js new file mode 100644 index 00000000..6f9d7fc6 --- /dev/null +++ b/client/components/boards/events.js @@ -0,0 +1,96 @@ +var toggleBoardStar = function(boardId) { + var queryType = Meteor.user().hasStarred(boardId) ? '$pull' : '$addToSet'; + var query = {}; + query[queryType] = { + 'profile.starredBoards': boardId + }; + Meteor.users.update(Meteor.userId(), query); +}; + +Template.boards.events({ + 'click .js-star-board': function(evt) { + toggleBoardStar(this._id); + evt.preventDefault(); + } +}); + +Template.headerBoard.events({ + 'click .js-star-board': function() { + toggleBoardStar(this._id); + }, + 'click .js-open-board-menu': Popup.open('boardMenu'), + 'click #permission-level:not(.no-edit)': Popup.open('boardChangePermission'), + 'click .js-filter-cards-indicator': function(evt) { + Session.set('currentWidget', 'filter'); + evt.preventDefault(); + }, + 'click .js-filter-card-clear': function(evt) { + Filter.reset(); + evt.stopPropagation(); + } +}); + +Template.boardMenuPopup.events({ + 'click .js-rename-board': Popup.open('boardChangeTitle'), + 'click .js-change-board-color': Popup.open('boardChangeColor') +}); + +Template.createBoardPopup.events({ + 'submit #CreateBoardForm': function(evt, t) { + var title = t.$('#boardNewTitle'); + + // trim value title + if ($.trim(title.val())) { + // İnsert Board title + var boardId = Boards.insert({ + title: title.val(), + permission: 'public' + }); + + // Go to Board _id + Utils.goBoardId(boardId); + } + evt.preventDefault(); + } +}); + +Template.boardChangeTitlePopup.events({ + 'submit #ChangeBoardTitleForm': function(evt, t) { + var title = t.$('.js-board-name').val().trim(); + if (title) { + Boards.update(this._id, { + $set: { + title: title + } + }); + Popup.close(); + } + evt.preventDefault(); + } +}); + +Template.boardChangePermissionPopup.events({ + 'click .js-select': function(evt) { + var $this = $(evt.currentTarget); + var permission = $this.attr('name'); + + Boards.update(this._id, { + $set: { + permission: permission + } + }); + Popup.close(); + } +}); + +Template.boardChangeColorPopup.events({ + 'click .js-select-background': function(evt) { + var currentBoardId = Session.get('currentBoard'); + Boards.update(currentBoardId, { + $set: { + color: this.toString() + } + }); + evt.preventDefault(); + } +}); diff --git a/client/components/boards/header.jade b/client/components/boards/header.jade new file mode 100644 index 00000000..189cdac4 --- /dev/null +++ b/client/components/boards/header.jade @@ -0,0 +1,87 @@ +template(name="headerBoard") + h1.header-board-menu.js-open-board-menu + = title + span.fa.fa-angle-down + + .board-header-btns.left + unless isSandstorm + a.board-header-btn.js-star-board(class="{{#if isStarred}}board-header-starred{{/if}}" + title="{{# if isStarred }}{{_ 'click-to-unstar'}}{{ else }}{{_ 'click-to-star'}}{{/ if }} {{_ 'starred-boards-description'}}") + span.board-header-btn-icon.icon-sm.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}") + //- XXX To implement + span.board-header-btn-text Starred + //- + XXX Normally we would disable this field for sandstorm, but we keep it + until sandstorm implements sharing capabilities + + a.board-header-btn.perms-btn.js-change-vis(class="{{#unless currentUser.isBoardAdmin}}no-edit{{/ unless}}" id="permission-level") + span.board-header-btn-icon.icon-sm.fa(class="{{#if isPublic}}fa-globe{{else}}fa-lock{{/if}}") + span.board-header-btn-text {{_ permission}} + + a.board-header-btn.js-search + span.board-header-btn-icon.icon-sm.fa.fa-tag + span.board-header-btn-text Labels + + //- XXX Clicking here should open a search field + a.board-header-btn.js-search + span.board-header-btn-icon.icon-sm.fa.fa-search + span.board-header-btn-text {{_ 'search'}} + + //- +boardMembersHeader + +template(name="boardMembersHeader") + .board-header-members + each currentBoard.members + +userAvatar(userId=userId draggable=true showBadges=true) + unless isSandstorm + if currentUser.isBoardAdmin + a.member.add-board-member.js-open-manage-board-members + i.fa.fa-plus + +template(name="boardMenuPopup") + ul.pop-over-list + li: a.js-rename-board {{_ 'rename-board'}} + li: a.js-change-board-color Change color + li: a Copy this board + li: a Rules + +template(name="boardChangeTitlePopup") + form#ChangeBoardTitleForm + label {{_ 'name'}} + input.js-board-name(type="text" value="{{ title }}" autofocus) + input.primary.wide.js-rename-board(type="submit" value="{{_ 'rename'}}") + +template(name="boardChangePermissionPopup") + ul.pop-over-list + li + a.js-select.light-hover(name="private") + span.icon-sm.fa.fa-lock.vis-icon + | {{_ 'private'}} + if check 'private' + span.icon-sm.fa.fa-check + span.sub-name {{_ 'private-desc'}} + li + a.js-select.light-hover(name="public") + span.icon-sm.fa.fa-globe.vis-icon + | {{_ 'public'}} + if check 'public' + span.icon-sm.fa.fa-check + span.sub-name {{_ 'public-desc'}} + +template(name="boardChangeColorPopup") + .board-backgrounds-list.clearfix + each backgroundColors + .board-background-select.js-select-background + span.background-box(class="board-color-{{this}}") + if isSelected + i.fa.fa-check + +template(name="createBoardPopup") + .content.clearfix + form#CreateBoardForm + label(for="boardNewTitle") {{_ 'title'}} + input#boardNewTitle.non-empty(type="text" name="name" placeholder="{{_ 'bucket-example'}}" autofocus) + p.quiet + span.icon-sm.fa.fa-globe + | {{{_ 'board-public-info'}}} + input.primary.wide(type="submit" value="{{_ 'create'}}") diff --git a/client/components/boards/header.js b/client/components/boards/header.js new file mode 100644 index 00000000..7d02df48 --- /dev/null +++ b/client/components/boards/header.js @@ -0,0 +1,7 @@ +Template.headerBoard.helpers({ + isStarred: function() { + var boardId = Session.get('currentBoard'); + var user = Meteor.user(); + return boardId && user && user.hasStarred(boardId); + } +}); diff --git a/client/components/boards/header.styl b/client/components/boards/header.styl new file mode 100644 index 00000000..44c38a4b --- /dev/null +++ b/client/components/boards/header.styl @@ -0,0 +1,137 @@ +@import 'nib' + +.board-header { + height: auto; + overflow: hidden; + padding: 10px 30px 10px 8px; + position: relative; + transition: padding .15s ease-in; +} + +.board-header-btns { + position: relative; + display: block; +} + +.board-header-btn { + border-radius: 3px; + color: #f6f6f6; + cursor: default; + float: left; + font-size: 12px; + height: 30px; + line-height: 32px; + margin: 2px 4px 0 0; + overflow: hidden; + padding-left: 30px; + position: relative; + text-decoration: none; +} + +.board-header-btn:empty { + display: none; +} + +.board-header-btn-without-icon { + padding-left: 8px; +} + +.board-header-btn-icon { + background-clip: content-box; + background-origin: content-box; + color: #f6f6f6 !important; + padding: 6px; + position: absolute; + top: 0; + left: 0; +} + +.board-header-btn-text { + padding-right: 8px; +} + +.board-header-btn:not(.no-edit) .text { + text-decoration: underline; +} + +.board-header-btn:not(.no-edit):hover { + background: rgba(0, 0, 0, .12); + cursor: pointer; +} + +.board-header-btn:hover { + color: #f6f6f6; +} + +.board-header-btn.board-header-btn-enabled { + background-color: rgba(0, 0, 0, .1); + + &:hover { + background-color: rgba(0, 0, 0, .3); + } + + .board-header-btn-icon.icon-star { + color: #e6bf00 !important; + } +} + +.board-header-btn-name { + cursor: default; + font-size: 18px; + font-weight: 700; + line-height: 30px; + padding-left: 4px; + text-decoration: none; + + .board-header-btn-text { + padding-left: 6px; + } +} + +.board-header-btn-name-org-logo { + border-radius: 3px; + height: 30px; + left: 0; + position: absolute; + top: 0; + width: 30px; + + .board-header-btn-text { + padding-left: 32px; + } +} + +.board-header-btn-org-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 400px; +} + +.board-header-btn-filter-indicator { + background: #3d990f; + padding-right: 30px; + color: #fff; + text-shadow: 0; + + &:hover { + background: #43a711 !important; + } + + .board-header-btn-icon-close { + background: #43a711; + border-top-left-radius: 0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 0; + color: #fff; + padding: 6px; + position: absolute; + right: 0; + top: 0; + + &:hover { + background: #48b512; + } + } +} diff --git a/client/components/boards/helpers.js b/client/components/boards/helpers.js new file mode 100644 index 00000000..05be987d --- /dev/null +++ b/client/components/boards/helpers.js @@ -0,0 +1,45 @@ +Template.boards.helpers({ + boards: function() { + return Boards.find({}, { + sort: ['title'] + }); + }, + + starredBoards: function() { + var cursor = Boards.find({ + _id: { $in: Meteor.user().profile.starredBoards || [] } + }, { + sort: ['title'] + }); + return cursor.count() === 0 ? null : cursor; + }, + + isStarred: function() { + var user = Meteor.user(); + return user && user.hasStarred(this._id); + } +}); + +Template.boardChangePermissionPopup.helpers({ + check: function(perm) { + return this.permission === perm; + } +}); + +Template.boardChangeColorPopup.helpers({ + backgroundColors: function() { + return Boards.simpleSchema()._schema.color.allowedValues; + }, + + isSelected: function() { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + return currentBoard.color === this.toString(); + } +}); + +Blaze.registerHelper('currentBoard', function() { + var boardId = Session.get('currentBoard'); + if (boardId) { + return Boards.findOne(boardId); + } +}); diff --git a/client/components/boards/list.jade b/client/components/boards/list.jade new file mode 100644 index 00000000..3a8fecd2 --- /dev/null +++ b/client/components/boards/list.jade @@ -0,0 +1,14 @@ +template(name="boards") + if boards + ul.board-list.clearfix + each boards + li(class="{{#if isStarred}}starred{{/if}}" class=colorClass) + a.js-open-board(href="{{ pathFor route='Board' boardId=_id }}") + span.details + span.board-list-item-name= title + i.fa.fa-star-o.js-star-board( + class="{{#if isStarred}}is-star-active{{/if}}" + title="{{_ 'star-board-title'}}") + else + p.quiet {{_ 'no-boards'}} + button.js-add-board {{_ 'add-board'}} diff --git a/client/components/boards/list.styl b/client/components/boards/list.styl new file mode 100644 index 00000000..c068dbb0 --- /dev/null +++ b/client/components/boards/list.styl @@ -0,0 +1,85 @@ +.board-list + margin: 25px auto + width: 1200px + + li + float: left + width: 25% + box-sizing: border-box + position: relative + + &.starred .fa-star-o + opacity: 1 + + a + background-color: #999 + color: #f6f6f6 + height: 90px + font-size: 16px + line-height: 22px + border-radius: 3px + display: block + font-weight: 700 + min-height: 18px + padding: 8px 12px 8px 12px + margin: 0 16px 16px 0 + position: relative + text-decoration: none + + &.tile + background-size: auto + background-repeat: repeat + + .details + height: 84px + padding-right: 36px + bottom: 0 + left: 0 + overflow: hidden + padding: 9px 12px + position: absolute + right: 0 + top: 0 + + .board-list-item-sub-name + color: rgba(255, 255, 255, .5) + display: block + font-size: 14px + font-weight: 400 + line-height: 22px + + .fa-star-o + bottom: 0 + font-size: 14px + height: 18px + line-height: 18px + opacity: 0 + padding: 9px 9px + position: absolute + right: 0 + top: 0 + transition-duration: .15s + transition-property: color, font-size, background + + .is-star-active + color: #e6bf00 + + li:hover a + color: #f6f6f6 + + .fa-star-o + color: #fff + opacity: .75 + + &:hover + font-size: 18px + opacity: 1 + + &.is-star-active + color: #e6bf00 + opacity: 1 + + &:hover + color: #ffd91a + font-size: 16px + opacity: 1 diff --git a/client/components/boards/router.js b/client/components/boards/router.js new file mode 100644 index 00000000..6845b7f2 --- /dev/null +++ b/client/components/boards/router.js @@ -0,0 +1,34 @@ +Meteor.subscribe('boards'); + +BoardSubsManager = new SubsManager(); + +Router.route('/boards', { + name: 'Boards', + template: 'boards', + authenticated: true, + onBeforeAction: function() { + Session.set('currentBoard', ''); + Filter.reset(); + this.next(); + } +}); + +Router.route('/boards/:_id/:slug', { + name: 'Board', + template: 'board', + onAfterAction: function() { + Session.set('sidebarIsOpen', true); + Session.set('currentWidget', 'home'); + Session.set('menuWidgetIsOpen', false); + }, + waitOn: function() { + var params = this.params; + Session.set('currentBoard', params._id); + Session.set('currentCard', null); + + return BoardSubsManager.subscribe('board', params._id, params.slug); + }, + data: function() { + return Boards.findOne(this.params._id); + } +}); diff --git a/client/components/cards/details.jade b/client/components/cards/details.jade new file mode 100644 index 00000000..0de59297 --- /dev/null +++ b/client/components/cards/details.jade @@ -0,0 +1,47 @@ +template(name="cardSidebar") + .card-sidebar.sidebar + .card-detail.sidebar-content.js-card-sidebar-content + if cover + .card-detail-cover(style="background-image: url({{ card.cover.url }})") + .card-detail-header(class="{{#if currentUser.isBoardMember}}editable{{/if}}") + a.js-close-card-detail + i.fa.fa-times + h2.card-detail-title.js-card-title= title + p.card-detail-list.js-move-card + | {{_ 'in-list'}} + a.card-detail-list-title( + class="{{#if currentUser.isBoardMember}}js-open-move-from-header is-editable{{/if}}") + = list.title + hr + //- if card.members + .card-detail-item.card-detail-item-members.clearfix.js-card-detail-members + h3.card-detail-item-header {{_ 'members'}} + .js-card-detail-members-list.clearfix + each members + +userAvatar(userId=this size="small" cardId=../_id) + a.card-detail-item-add-button.dark-hover.js-details-edit-members + i.fa.fa-plus + //- We should use "editable" to avoide repetiting ourselves + .clearfix + if currentUser.isBoardMember + h3 Description + +inlinedForm(classNames="js-card-description") + i.fa.fa-times.js-close-inlined-form + textarea(autofocus)= description + button(type="submit") {{_ 'edit'}} + else + .js-open-inlined-form + a {{_ 'edit'}} + +viewer + = description + else if description + h3 Description + +viewer + = description + hr + if attachments.count + +WindowAttachmentsModule(card=this) + +WindowActivityModule(card=this) + +template(name="moveCardPopup") + +boardLists diff --git a/client/components/cards/details.js b/client/components/cards/details.js new file mode 100644 index 00000000..a4fe89a3 --- /dev/null +++ b/client/components/cards/details.js @@ -0,0 +1,103 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'cardSidebar'; + }, + + mixins: function() { + return [Mixins.InfiniteScrolling]; + }, + + calculateNextPeak: function() { + var altitude = this.find('.js-card-sidebar-content').scrollHeight; + this.callFirstWith(this, 'setNextPeak', altitude); + }, + + reachNextPeak: function() { + var activitiesComponent = this.componentChildren('activities')[0]; + activitiesComponent.loadNextPage(); + }, + + events: function() { + return [{ + 'click .js-move-card': Popup.open('moveCard'), + 'submit .js-card-description': function(evt) { + evt.preventDefault(); + var cardId = Session.get('currentCard'); + var form = this.componentChildren('inlinedForm')[0]; + var newDescription = form.getValue(); + Cards.update(cardId, { + $set: { + description: newDescription + } + }); + form.close(); + }, + 'click .js-close-card-detail': function() { + Utils.goBoardId(Session.get('currentBoard')); + }, + 'click .editable .js-card-title': function(event, t) { + var editable = t.$('.card-detail-title'); + + // add class editing and focus + $('.editing').removeClass('editing'); + editable.addClass('editing'); + editable.find('#title').focus(); + }, + 'click .js-edit-desc': function(event, t) { + var editable = t.$('.card-detail-item'); + + // editing remove based and add current editing. + $('.editing').removeClass('editing'); + editable.addClass('editing'); + editable.find('#desc').focus(); + + event.preventDefault(); + }, + 'click .js-cancel-edit': function(event, t) { + // remove editing hide. + $('.editing').removeClass('editing'); + }, + 'submit #WindowTitleEdit': function(event, t) { + var title = t.find('#title').value; + if ($.trim(title)) { + Cards.update(this.card._id, { + $set: { + title: title + } + }, function (err, res) { + if (!err) $('.editing').removeClass('editing'); + }); + } + + event.preventDefault(); + }, + 'submit #WindowDescEdit': function(event, t) { + Cards.update(this.card._id, { + $set: { + description: t.find('#desc').value + } + }, function(err) { + if (!err) $('.editing').removeClass('editing'); + }); + event.preventDefault(); + }, + 'click .member': Popup.open('cardMember'), + 'click .js-details-edit-members': Popup.open('cardMembers'), + 'click .js-details-edit-labels': Popup.open('cardLabels') + }]; + } +}).register('cardSidebar'); + +Template.moveCardPopup.events({ + 'click .js-select-list': function() { + // XXX We should *not* get the currentCard from the global state, but + // instead from a “component” state. + var cardId = Session.get('currentCard'); + var newListId = this._id; + Cards.update(cardId, { + $set: { + listId: newListId + } + }); + } +}); diff --git a/client/components/cards/details.styl b/client/components/cards/details.styl new file mode 100644 index 00000000..faf15d79 --- /dev/null +++ b/client/components/cards/details.styl @@ -0,0 +1,161 @@ +@import 'nib' + +.card-detail.sidebar-content + width: 496px - 2 * 20px + top: -46px !important + z-index: 20 !important + // XXX Animate apparition + + .card-detail-header + background: #F7F7F7 + border-bottom: 1px solid darken(white, 10%) + position: absolute + min-height: 38px + top: 0 + left: 0 + right: 0 + padding 7px 20px 0 + + i.fa + float: right + font-size: 1.3em + color: darken(white, 35%) + margin-top: 7px + + .card-detail-title + font-weight: bold + font-size: 1.7em + margin: 3px 0 0 + padding: 0 + + .card-detail-list + font-size: 0.85em + margin-bottom: 3px + + a.card-detail-list-title + font-weight: bold + + &.is-editable + display: inline-block + background: darken(white, 10%) + border-radius: 3px + padding: 0px 5px + +.new-comment + position: relative + margin: 0 0 20px 38px + + .member + opacity: .7 + position: absolute + top: 1px + left: -38px + + .helper + bottom: 0 + display: none + position: absolute + right: 9px + + &.focus + + .member + opacity: 1 + + .helper + display: inline-block + + .new-comment-input + min-height: 108px + color: #4d4d4d + cursor: auto + overflow: hidden + word-wrap: break-word + + .too-long + margin-top: 8px + +.new-comment-input + background-color: #fff + border: 0 + box-shadow: 0 1px 2px rgba(0, 0, 0, .23) + color: #8c8c8c + height: 36px + margin: 4px 4px 6px 0 + padding: 9px 11px + width: 100% + + &:hover, + &:focus + background-color: #fff + box-shadow: 0 1px 3px rgba(0, 0, 0, .33) + border: 0 + cursor: pointer + + &:focus + cursor: auto + +.list-voters.compact .voter + position: relative + min-height: 36px + + .member + left: 0 + position: absolute + top: 0 + + .title + display: block + line-height: 30px + left: 0 + overflow: hidden + padding-left: 38px + position: absolute + text-overflow: ellipsis + top: 0 + white-space: nowrap + width: 230px + +.list-voters .title + display: none + +.card-composer + padding-bottom: 8px + +.cc-controls + margin-top: 1px + + input[type="submit"] + float: left + margin-top: 0 + padding: 5px 18px + + .icon-lg + float: left + + .cc-opt + float: right + +.minicard-placeholder, +.minicard.placeholder + background: silver + border: none + min-height: 18px + + .hook + height: 18px + position: absolute + right: 0 + top: 0 + width: 18px + +input[type="text"].attachment-add-link-input + float: left + margin: 0 0 8px + width: 80% + +input[type="submit"].attachment-add-link-submit + float: left + margin: 0 0 8px 4px + padding: 6px 12px + width: 18% diff --git a/client/components/cards/events.js b/client/components/cards/events.js new file mode 100644 index 00000000..9c270e8d --- /dev/null +++ b/client/components/cards/events.js @@ -0,0 +1,285 @@ +// Template.cards.events({ +// // 'click .js-cancel': function(event, t) { +// // var composer = t.$('.card-composer'); + +// // // Keep the old value in memory to display it again next time +// // var inputCacheKey = "addCard-" + this.listId; +// // var oldValue = composer.find('.js-card-title').val(); +// // InputsCache.set(inputCacheKey, oldValue); + +// // // add composer hide class +// // composer.addClass('hide'); +// // composer.find('.js-card-title').val(''); + +// // // remove hide open link class +// // $('.js-open-card-composer').removeClass('hide'); +// // }, +// 'submit': function(evt, tpl) { +// evt.preventDefault(); +// var textarea = $(evt.currentTarget).find('textarea'); +// var title = textarea.val(); +// var lastCard = tpl.find('.js-minicard:last-child'); +// var sort; +// if (lastCard === null) { +// sort = 0; +// } else { +// sort = Blaze.getData(lastCard).sort + 1; +// } +// // debugger + +// // Clear the form in-memory cache +// // var inputCacheKey = "addCard-" + this.listId; +// // InputsCache.set(inputCacheKey, ''); + +// // title trim if not empty then +// if ($.trim(title)) { +// Cards.insert({ +// title: title, +// listId: Template.currentData().listId, +// boardId: Template.currentData().board._id, +// sort: sort +// }, function(err, _id) { +// // In case the filter is active we need to add the newly +// // inserted card in the list of exceptions -- cards that are +// // not filtered. Otherwise the card will disappear instantly. +// // See https://github.com/libreboard/libreboard/issues/80 +// Filter.addException(_id); +// }); + +// // empty and focus. +// textarea.val('').focus(); + +// // focus complete then scroll top +// Utils.Scroll(tpl.find('.js-minicards')).top(1000, true); +// } +// } +// }); + +// Template.cards.events({ +// 'click .member': Popup.open('cardMember') +// }); + +Template.cardMemberPopup.events({ + 'click .js-remove-member': function() { + Cards.update(this.cardId, {$pull: {members: this.userId}}); + Popup.close(); + } +}); + +Template.WindowActivityModule.events({ + 'click .js-new-comment:not(.focus)': function(evt) { + var $this = $(evt.currentTarget); + $this.addClass('focus'); + }, + 'submit #CommentForm': function(evt, t) { + var text = t.$('.js-new-comment-input'); + if ($.trim(text.val())) { + CardComments.insert({ + boardId: this.card.boardId, + cardId: this.card._id, + text: text.val() + }); + text.val(''); + $('.focus').removeClass('focus'); + } + evt.preventDefault(); + } +}); + +Template.WindowSidebarModule.events({ + 'click .js-change-card-members': Popup.open('cardMembers'), + 'click .js-edit-labels': Popup.open('cardLabels'), + 'click .js-archive-card': function(evt) { + // Update + Cards.update(this.card._id, { + $set: { + archived: true + } + }); + evt.preventDefault(); + }, + 'click .js-unarchive-card': function(evt) { + Cards.update(this.card._id, { + $set: { + archived: false + } + }); + evt.preventDefault(); + }, + 'click .js-delete-card': Popup.afterConfirm('cardDelete', function() { + Cards.remove(this.card._id); + + // redirect board + Utils.goBoardId(this.card.board()._id); + Popup.close(); + }), + 'click .js-more-menu': Popup.open('cardMore'), + 'click .js-attach': Popup.open('cardAttachments') +}); + +Template.WindowAttachmentsModule.events({ + 'click .js-attach': Popup.open('cardAttachments'), + 'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', + function() { + Attachments.remove(this._id); + Popup.close(); + } + ), + // If we let this event bubble, Iron-Router will handle it and empty the + // page content, see #101. + 'click .js-open-viewer, click .js-download': function(event) { + event.stopPropagation(); + }, + 'click .js-add-cover': function() { + Cards.update(this.cardId, { $set: { coverId: this._id } }); + }, + 'click .js-remove-cover': function() { + Cards.update(this.cardId, { $unset: { coverId: '' } }); + } +}); + +Template.cardMembersPopup.events({ + 'click .js-select-member': function(evt) { + var cardId = Template.parentData(2).data._id; + var memberId = this.userId; + var operation; + if (Cards.find({ _id: cardId, members: memberId}).count() === 0) + operation = '$addToSet'; + else + operation = '$pull'; + + var query = {}; + query[operation] = { + members: memberId + }; + Cards.update(cardId, query); + evt.preventDefault(); + } +}); + +Template.cardLabelsPopup.events({ + 'click .js-select-label': function(evt) { + var cardId = Template.parentData(2).data._id; + var labelId = this._id; + var operation; + if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0) + operation = '$addToSet'; + else + operation = '$pull'; + + var query = {}; + query[operation] = { + labelIds: labelId + }; + Cards.update(cardId, query); + evt.preventDefault(); + }, + 'click .js-edit-label': Popup.open('editLabel'), + 'click .js-add-label': Popup.open('createLabel') +}); + +Template.formLabel.events({ + 'click .js-palette-color': function(evt) { + var $this = $(evt.currentTarget); + + // hide selected ll colors + $('.js-palette-select').addClass('hide'); + + // show select color + $this.find('.js-palette-select').removeClass('hide'); + } +}); + +Template.createLabelPopup.events({ + // Create the new label + 'submit .create-label': function(evt, tpl) { + var name = tpl.$('#labelName').val().trim(); + var boardId = Session.get('currentBoard'); + var selectLabelDom = tpl.$('.js-palette-select:not(.hide)').get(0); + var selectLabel = Blaze.getData(selectLabelDom); + Boards.update(boardId, { + $push: { + labels: { + _id: Random.id(6), + name: name, + color: selectLabel.color + } + } + }); + Popup.back(); + evt.preventDefault(); + } +}); + +Template.editLabelPopup.events({ + 'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() { + var boardId = Session.get('currentBoard'); + Boards.update(boardId, { + $pull: { + labels: { + _id: this._id + } + } + }); + Popup.back(2); + }), + 'submit .edit-label': function(evt, tpl) { + var name = tpl.$('#labelName').val().trim(); + var boardId = Session.get('currentBoard'); + var getLabel = Utils.getLabelIndex(boardId, this._id); + var selectLabelDom = tpl.$('.js-palette-select:not(.hide)').get(0); + var selectLabel = Blaze.getData(selectLabelDom); + var $set = {}; + + // set label index + $set[getLabel.key('name')] = name; + + // set color + $set[getLabel.key('color')] = selectLabel.color; + + // update + Boards.update(boardId, { $set: $set }); + + // return to the previous popup view trigger + Popup.back(); + + evt.preventDefault(); + }, + 'click .js-select-label': function() { + Cards.remove(this.cardId); + + // redirect board + Utils.goBoardId(this.boardId); + } +}); + +Template.cardMorePopup.events({ + 'click .js-delete': Popup.afterConfirm('cardDelete', function() { + Cards.remove(this.card._id); + + // redirect board + Utils.goBoardId(this.card.board()._id); + }) +}); + +Template.cardAttachmentsPopup.events({ + 'change .js-attach-file': function(evt) { + var card = this.card; + FS.Utility.eachFile(evt, function(f) { + var file = new FS.File(f); + + // set Ids + file.boardId = card.boardId; + file.cardId = card._id; + + // upload file + Attachments.insert(file); + + Popup.close(); + }); + }, + 'click .js-computer-upload': function(evt, t) { + t.find('.js-attach-file').click(); + evt.preventDefault(); + } +}); diff --git a/client/components/cards/helpers.js b/client/components/cards/helpers.js new file mode 100644 index 00000000..708b1b56 --- /dev/null +++ b/client/components/cards/helpers.js @@ -0,0 +1,48 @@ +Template.cardMembersPopup.helpers({ + isCardMember: function() { + var cardId = Template.parentData()._id; + var cardMembers = Cards.findOne(cardId).members || []; + return _.contains(cardMembers, this.userId); + }, + user: function() { + return Users.findOne(this.userId); + } +}); + +Template.cardLabelsPopup.helpers({ + isLabelSelected: function(cardId) { + return _.contains(Cards.findOne(cardId).labelIds, this._id); + } +}); + +var labelColors; +Meteor.startup(function() { + labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; +}); + +Template.createLabelPopup.helpers({ + // This is the default color for a new label. We search the first color that + // is not already used in the board (although it's not a problem if two + // labels have the same color). + defaultColor: function() { + var labels = this.labels || this.card.board().labels; + var usedColors = _.pluck(labels, 'color'); + var availableColors = _.difference(labelColors, usedColors); + return availableColors.length > 1 ? availableColors[0] : 'green'; + } +}); + +Template.formLabel.helpers({ + labels: function() { + return _.map(labelColors, function(color) { + return { color: color, name: '' }; + }); + } +}); + +Blaze.registerHelper('currentCard', function() { + var cardId = Session.get('currentCard'); + if (cardId) { + return Cards.findOne(cardId); + } +}); diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl new file mode 100644 index 00000000..27058b21 --- /dev/null +++ b/client/components/cards/labels.styl @@ -0,0 +1,183 @@ +@import 'nib' + +// XXX Use .board-widget-labels as a flexbox container +.card-label + background-color: #b3b3b3 + border-radius: 4px + color: white + display: inline-block + font-weight: 700 + font-size: 13px + margin-right: 4px + padding: 3px 8px + position:relative + max-width: 100% + min-width: 8px + overflow: ellipsis + height: 18px + + &:hover + color: white + +.card-label-green + background-color: #3cb500 + +.card-label-yellow + background-color: #fad900 + +.card-label-orange + background-color: #ff9f19 + +.card-label-red + background-color: #eb4646 + +.card-label-purple + background-color: #a632db + +.card-label-blue + background-color: #0079bf + +.card-label-pink + background-color: #ff78cb + +.card-label-sky + background-color: #00c2e0 + +.card-label-black + background-color: #4d4d4d + +.card-label-lime + background-color: #51e898 + +.edit-label, +.create-label + .card-label + float: left + height: 25px + margin: 0px 3% 7px 0px + width: 10.5% + cursor: pointer + +.edit-labels + input[type="text"] + margin: 4px 0 6px 38px + width: 243px + + .card-label + height: 30px + left: 0 + padding: 1px 5px + position: absolute + top: 0 + width: 24px + + .labels-static .card-label + line-height: 30px + margin-bottom: 4px + position: relative + top: auto + left: 0 + width: 260px + +.minicard-labels + position: relative + z-index: 30 + top: -6px + + .card-label + border-radius: 0 + float: left + height: 4px + margin-bottom: 1px + padding: 0 + width: 40px + line-height: 100px + +.card-detail-item-labels .card-label + border-radius: 3px + display: block + float: left + height: 20px + line-height: 20px + margin: 0 4px 4px 0 + min-width: 30px + padding: 5px 10px + width: auto + +.editable-labels .card-label:hover + cursor: pointer + opacity: .75 + +.edit-labels-pop-over + margin-bottom: 8px + +.edit-labels-pop-over .shortcut + display: inline-block + +.card-label-selectable + border-radius: 3px + cursor: pointer + margin: 0 50px 4px 0 + min-height: 18px + padding: 8px + position: relative + transition: margin-right .1s + + .card-label-selectable-icon + position: absolute + top: 8px + right: -20px + + &.active:hover, + &.active, + &.active.selected:hover, + &.active.selected + margin-right: 38px + padding-right: 32px + + .card-label-selectable-icon + right: 6px + + &.active:hover:hover, + &.active:hover, + &.active.selected:hover:hover, + &.active.selected:hover + margin-right: 38px + + &.selected, + &:hover + margin-right: 38px + opacity: .8 + +.active .card-label-selectable + &, + &:hover + margin-right: 0 + + .card-label-selectable-icon + right: 8px + +.card-label-edit-button + border-radius: 3px + float: right + padding: 8px + + &:hover + background: #dbdbdb + +.card-label-color-select-icon + left: 14px + position: absolute + top: 9px + +.phenom .card-label + display: inline-block + font-size: 12px + height: 14px + line-height: 13px + padding: 0 4px + min-width: 16px + overflow: ellipsis + +.board-widget .phenom .card-label + max-width: 130px diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl new file mode 100644 index 00000000..a78cd46f --- /dev/null +++ b/client/components/cards/minicard.styl @@ -0,0 +1,136 @@ +.minicard + background-color: #fff + box-shadow: 0 1px 2px rgba(0,0,0,.2) + border-radius: 2px + cursor: pointer + margin-bottom: 9px + max-width: 300px + min-height: 20px + position: relative + z-index: 0 + overflow: hidden + + a + color: #4d4d4d + + &.active-card + background-color: #f0f0f0 + border-bottom-color: #c2c2c2 + + .minicard-operation + display: block + + &.draggable-hover-card + background-color: #f0f0f0 + border-bottom-color: #c2c2c2 + + .minicard-cover + background-position: center + background-repeat: no-repeat + background-size: cover + height: 145px + user-select: none + margin: -6px -8px 6px -8px + border-radius: top 2px + + &.no-preview-size + background-size: auto + background-position: center + + .minicard-details + padding: 6px 8px 2px + position: relative + z-index: 10 + + + &.is-selected + .minicard-details + padding-bottom: 0 + + a.minicard-details + text-decoration:none + + .minicard-details-overlay + background: transparent + bottom: 0 + left: 0 + position: absolute + right: 0 + top: 0 + + .minicard-dropzone + display: none + + .minicard.drophover .minicard-dropzone + background: rgba(255, 255, 255, .8) + // border-radius: 3px + // bottom: 0 + // display: block + // font-weight: 700 + // line-height: 100% + // left: 0 + // margin: 0 + // opacity: 1 + // padding: 0 + // position: absolute + // right: 0 + // text-align: center + // top: 0 + // z-index: 40 + + .minicard-title + display: block + font-weight: 400 + margin: 0 0 4px + overflow: hidden + text-decoration: none + word-wrap: break-word + + &::selection + background: transparent + + .minicard-labels + padding-top: 3px + margin-top: 4px + float: right + + .minicard-label + float: right + width: 8px + height: @width + border-radius: 2px + margin-left: 4px + + .minicard-members + float: right + margin: 2px -8px -2px 0 + + .member + float: right + border-radius: 50% + height: 28px + width: @height + + + .badges + margin-top: 10px + + .minicard-members:empty + display: none + +.badges + float: left + + &:empty + display: none + +textarea.minicard-composer-textarea, +textarea.minicard-composer-textarea:focus + background: none + border: none + box-shadow: none + height: auto + margin-bottom: 4px + padding: 0 + max-height: 162px + min-height: 54px + overflow-y: auto diff --git a/client/components/cards/popups.jade b/client/components/cards/popups.jade new file mode 100644 index 00000000..0b5aa4c0 --- /dev/null +++ b/client/components/cards/popups.jade @@ -0,0 +1,12 @@ +template(name="cardMembersPopup") + //- input.js-search-mem(autofocus placeholder="Search members…" type="text") + ul.pop-over-member-list.checkable.js-mem-list + each board.members + li.item.js-member-item(class="{{#if isCardMember}}active{{/if}}") + a.name.js-select-member(href="#") + +userAvatar(user=user size="small") + span.full-name + = user.profile.name + | (<span class="username">{{ user.username }}</span>) + if isCardMember + i.fa.fa-check diff --git a/client/components/cards/router.js b/client/components/cards/router.js new file mode 100644 index 00000000..48bb9a95 --- /dev/null +++ b/client/components/cards/router.js @@ -0,0 +1,15 @@ +Router.route('/boards/:boardId/:slug/:cardId', { + name: 'Card', + template: 'board', + waitOn: function() { + var params = this.params; + // XXX We probably shouldn't rely on Session + Session.set('currentBoard', params.boardId); + Session.set('currentCard', params.cardId); + + return BoardSubsManager.subscribe('board', params.boardId, params.slug); + }, + data: function() { + return Boards.findOne(this.params.boardId); + } +}); diff --git a/client/components/cards/templates.html b/client/components/cards/templates.html new file mode 100644 index 00000000..4c65e429 --- /dev/null +++ b/client/components/cards/templates.html @@ -0,0 +1,336 @@ +<template name="cardModal"> + {{ > modal template='cardDetailWindow' card=this board=this.board }} +</template> + +<template name="cardMemberPopup"> + <div class="board-member-menu"> + <div class="mini-profile-info"> + {{> userAvatar user=user }} + <div class="info"> + <h3 class="bottom" style="margin-right: 40px;"> + <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a> + </h3> + <p class="quiet bottom">@{{ user.username }}</p> + </div> + </div> + {{# if currentUser.isBoardMember }} + <ul class="pop-over-list"> + <li><a class="js-remove-member">{{_ 'remove-member-from-card'}}</a></li> + </ul> + {{/ if }} + </div> +</template> + +<template name="cardMorePopup"> + <p class="quiet bottom"> + <span class="clearfix"> + <span>{{_ 'link-card'}}</span> + <span class="icon-sm fa {{#if card.board.isPublic}}fa-globe{{else}}fa-lock{{/if}}"></span> + <input class="js-url js-autoselect inline-input" type="text" readonly="readonly" value="{{ card.rootUrl }}"> + </span> + {{_ 'added'}} <span class="date" title="{{ card.createdAt }}">{{ moment card.createdAt 'LLL' }}</span> - + <a class="js-delete" href="#" title="{{_ 'card-delete-notice'}}">{{_ 'delete'}}</a> + </p> +</template> + +<template name="cardLabelsPopup"> + <div> + {{! <input id="labelSearch" name="search" class="js-autofocus js-label-search" placeholder="Search labels…" value="" type="text"> }} + <ul class="edit-labels-pop-over js-labels-list"> + {{# each card.board.labels }} + <li> + <a href="#" class="card-label-edit-button icon-sm fa fa-pencil js-edit-label"></a> + <span class="card-label card-label-selectable card-label-{{color}} js-select-label {{# if isLabelSelected ../card._id }}active{{/ if }}"> + {{name}} + {{# if currentUser.isBoardAdmin }} + <span class="card-label-selectable-icon icon-sm fa fa-check light"></span> + {{/ if }} + </span> + </li> + {{/ each}} + </ul> + <a class="quiet-button full js-add-label">{{_ 'label-create'}}</a> + </div> +</template> + +<template name="cardAttachmentsPopup"> + <div> + <ul class="pop-over-list"> + <li> + <input type="file" name="file" class="js-attach-file hide" multiple> + <a class="js-computer-upload" href="#"> + {{_ 'computer'}} + </a> + </li> + </ul> + </div> +</template> + +<template name="formLabel"> + <div class="colors clearfix"> + <label for="labelName">{{_ 'name'}}</label> + <input id="labelName" type="text" name="name" class="js-label-name" value='{{ name }}' autofocus> + <label>{{_ "select-color"}}</label> + {{# each labels }} + <span class="card-label card-label--selectable card-label-{{ color }} palette-color js-palette-color"> + <span class="card-label-color-select-icon icon-sm fa fa-check light js-palette-select {{#if $neq color ../color}}hide{{/if}}"></span> + </span> + {{/each}} + </div> +</template> + +<template name="createLabelPopup"> + <form class="create-label"> + {{#with color=defaultColor}} + {{> formLabel}} + {{/with}} + <input type="submit" class="primary wide left" value="{{_ 'create'}}"> + </form> +</template> + +<template name="editLabelPopup"> + <form class="edit-label"> + {{> formLabel}} + <input type="submit" class="primary wide left" value="{{_ 'save'}}"> + <span class="right"> + <input type="submit" value="{{_ 'delete'}}" class="negate js-delete-label"> + </span> + </form> +</template> + +<template name="deleteLabelPopup"> + <p>{{_ "label-delete-pop"}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}"> +</template> + +<template name="cardDeletePopup"> + <p>{{_ "card-delete-pop"}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}"> +</template> + +<template name="attachmentDeletePopup"> + <p>{{_ "attachment-delete-pop"}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'delete'}}"> +</template> + +<template name="cardDetailSidebarOld"> + <div class="card-detail-window clearfix"> + {{# if card.cover }} + <div class="window-cover js-card-cover-box js-open-card-cover-in-viewer has-cover" style="background-image: url({{ card.cover.url }}); background-color: rgb(119, 119, 119); background-size: contain;"> + </div> + {{ /if }} + {{ #if card.archived }} + <div class="window-archive-banner js-archive-banner"> + <span class="icon-lg fa fa-archive window-archive-banner-icon"></span> + <p class="window-archive-banner-text">{{_ "card-archived"}}</p> + </div> + {{ /if }} + <div class="window-header clearfix"> + <span class="window-header-icon icon-lg fa fa-calendar-o"></span> + <div class="window-title card-detail-title non-empty inline {{# if currentUser.isBoardMember }}editable{{/ if }}"> + <h2 class="window-title-text current hide-on-edit js-card-title">{{ card.title }}</h2> + <div class="edit edit-heavy"> + <form id="WindowTitleEdit"> + <textarea type="text" class="field single-line" id="title">{{ card.title }}</textarea> + <div class="edit-controls clearfix"> + <input type="submit" class="primary confirm js-title-save-edit" value="{{_ 'save'}}"> + <a href="#" class="icon-lg fa fa-times dark-hover cancel js-cancel-edit"></a> + </div> + </form> + </div> + <div class="quiet hide-on-edit window-header-inline-content js-current-list"> + <p class="inline-block bottom"> + {{_ 'in-list'}} + <a href="#" class="{{# if currentUser.isBoardMember }}js-open-move-from-header{{else}}disabled{{/ if }}"><strong>{{ card.list.title }}</strong></a> + </p> + </div> + </div> + </div> + <div class="window-main-col clearfix"> + <div class="card-detail-data gutter clearfix"> + <div class="card-detail-item card-detail-item-block clear clearfix editable"> + {{# if card.members }} + <div class="card-detail-item card-detail-item-members clearfix js-card-detail-members"> + <h3 class="card-detail-item-header">{{_ 'members'}}</h3> + <div class="js-card-detail-members-list clearfix"> + {{# each card.members }} + {{> userAvatar userId=this size="small" cardId=../card._id }} + {{/ each }} + <a class="card-detail-item-add-button dark-hover js-details-edit-members"> + <span class="icon-sm fa fa-plus"></span> + </a> + </div> + </div> + {{/ if }} + {{# if card.labels }} + <div class="card-detail-item card-detail-item-labels clearfix js-card-detail-labels"> + <h3 class="card-detail-item-header">{{_ 'labels'}}</h3> + <div class="js-card-detail-labels-list clearfix editable-labels js-edit-label"> + {{# each card.labels }} + <span class="card-label card-label-{{color}}" title="{{name}}">{{ name }}</span> + {{/ each }} + <a class="card-detail-item-add-button dark-hover js-details-edit-labels"> + <span class="icon-sm fa fa-plus"></span> + </a> + </div> + </div> + {{/ if }} + <div class="card-detail-item card-detail-item-block clear clearfix editable" attr="desc"> + {{# if card.description }} + <h3 class="card-detail-item-header js-show-with-desc">{{_ 'description'}}</h3> + {{# if currentUser.isBoardMember }} + <a href="#" class="card-detail-item-header-edit hide-on-edit js-show-with-desc js-edit-desc">{{_ 'edit'}}</a> + {{/ if }} + <div class="current markeddown hide-on-edit js-card-desc js-show-with-desc"> + {{#viewer}}{{ card.description }}{{/viewer}} + </div> + {{ else }} + {{# if currentUser.isBoardMember }} + <p class="bottom"> + <a href="#" class="hide-on-edit quiet-button w-img js-edit-desc js-hide-with-desc"> + <span class="icon-sm fa fa-align-left"></span> + {{_ 'edit-description'}} + </a> + </p> + {{/ if }} + {{/ if }} + <div class="card-detail-edit edit"> + <form id="WindowDescEdit"> + {{#editor class="field single-line2" id="desc"}}{{ card.description }}{{/editor}} + <div class="edit-controls clearfix"> + <input type="submit" class="primary confirm js-title-save-edit" value="{{_ 'save'}}"> + <a href="#" class="icon-lg fa fa-times dark-hover cancel js-cancel-edit"></a> + </div> + </form> + </div> + </div> + </div> + </div> + {{# if card.attachments.count }} + {{ > WindowAttachmentsModule card=card }} + {{/ if}} + {{ > WindowActivityModule card=card }} + </div> + {{# if currentUser.isBoardMember }} + {{ > WindowSidebarModule card=card }} + {{/if}} + </div> +</template> + +<template name="WindowActivityModule"> + <div class="card-detailwindow-module"> + <div class="window-module-title window-module-title-no-divider"> + <span class="window-module-title-icon icon-lg fa fa-comments-o"></span> + <h3>{{ _ 'activity'}}</h3> + </div> + {{# if currentUser.isBoardMember }} + <div class="new-comment js-new-comment"> + {{> userAvatar user=currentUser size="small" class="member-no-menu" }} + <form id="CommentForm"> + {{#editor class="new-comment-input js-new-comment-input"}}{{/editor}} + <div class="add-controls clearfix"> + <input type="submit" class="primary confirm clear js-add-comment" value="{{_ 'comment'}}" tabindex="2"> + </div> + </form> + </div> + {{/ if }} + {{ > activities mode="card" }} + </div> +</template> + +<template name="WindowAttachmentsModule"> + <div class="window-module js-attachments-section clearfix"> + <div class="window-module-title window-module-title-no-divider"> + <span class="window-module-title-icon icon-lg fa fa-paperclip"></span> + <h3 class="inline-block">{{_ 'attachments'}}</h3> + </div> + <div class="gutter"> + <div class="clearfix js-attachment-list"> + {{# each card.attachments }} + <div class="attachment-thumbnail"> + {{# if isUploaded }} + <a href="{{ url download=true }}" class="attachment-thumbnail-preview js-open-viewer attachment-thumbnail-preview-is-cover"> + {{# if isImage }} + <img src="{{ url }}"> + {{ else }} + <span class="attachment-thumbnail-preview-ext">{{ extension }}</span> + {{ /if }} + </a> + <p class="attachment-thumbnail-details js-open-viewer"> + <a href="" class="attachment-thumbnail-details-title js-attachment-thumbnail-details"> + {{ name }} + <span class="block quiet"> + {{_ 'added'}} <span class="date">{{ moment uploadedAt }}</span> + </span> + </a> + <span class="quiet attachment-thumbnail-details-options"> + <a href="{{ url download=true }}" class="attachment-thumbnail-details-options-item dark-hover js-download"> + <span class="icon-sm fa fa-download"></span> + <span class="attachment-thumbnail-details-options-item-text">{{_ 'download'}}</span> + </a> + {{# if isImage }} + <a class="attachment-thumbnail-details-options-item dark-hover {{#if $eq ../card.coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}"> + <span class="icon-sm fa fa-thumb-tack"></span> + <span class="attachment-thumbnail-details-options-item-text">{{#if $eq ../card.coverId _id}}{{_ 'remove-cover'}}{{else}}{{_ 'add-cover'}}{{/if}}</span> + </a> + {{/if}} + <a href="#" class="attachment-thumbnail-details-options-item attachment-thumbnail-details-options-item-delete dark-hover js-confirm-delete"> + <span class="icon-sm fa fa-close"></span> + <span class="attachment-thumbnail-details-options-item-text">{{_ 'delete'}}</span> + </a> + </span> + </p> + {{ else }} + +spinner + {{/ if }} + </div> + {{/each}} + </div> + <p> + <a href="#" class="quiet-button js-attach">{{_ 'add-attachment' }}</a> + </p> + </div> + </div> +</template> + +<template name="WindowSidebarModule"> + <div class="window-sidebar" style="position: relative;"> + <div class="window-module clearfix"> + <h3>{{_ 'add'}}</h3> + <div class="clearfix"> + <a href="#" class="button-link js-change-card-members" title="{{_ 'members-title'}}"> + <span class="icon-sm fa fa-user"></span> {{_ 'members'}} + </a> + <a href="#" class="button-link js-edit-labels" title="{{_ 'labels-title'}}"> + <span class="icon-sm fa fa-tags"></span> {{_ 'labels'}} + </a> + <a href="#" class="button-link js-attach" title="{{_ 'attachment-title'}}"> + <span class="icon-sm fa fa-paperclip"></span> {{_ 'attachment'}} + </a> + </div> + </div> + <div class="window-module other-actions clearfix"> + <h3>{{_ 'actions'}}</h3> + <div class="clearfix"> + <hr> + {{ #if card.archived }} + <a href="#" class="button-link js-unarchive-card" title="{{_ 'send-to-board-title'}}"> + <span class="icon-sm fa fa-recycle"></span> {{_ 'send-to-board'}} + </a> + <a href="#" class="button-link negate js-delete-card" title="{{_ 'delete-title'}}"> + <span class="icon-sm fa fa-trash-o"></span> {{_ 'delete'}} + </a> + {{ else }} + <a href="#" class="button-link js-archive-card" title="{{_ 'archive-title'}}"> + <span class="icon-sm fa fa-archive"></span> {{_ 'archive'}} + </a> + {{ /if }} + </div> + </div> + <div class="window-module clearfix"> + <p class="quiet bottom"> + <a href="#" class="quiet-button js-more-menu" title="{{_ 'share-and-more-title'}}">{{_ 'share-and-more'}}</a> + </p> + </div> + </div> +</template> diff --git a/client/components/forms/cachedValue.js b/client/components/forms/cachedValue.js new file mode 100644 index 00000000..a2898d85 --- /dev/null +++ b/client/components/forms/cachedValue.js @@ -0,0 +1,22 @@ +var emptyValue = ''; + +Mixins.CachedValue = BlazeComponent.extendComponent({ + onCreated: function() { + this._cachedValue = emptyValue; + }, + + setCache: function(value) { + this._cachedValue = value; + }, + + getCache: function(defaultValue) { + if (this._cachedValue === emptyValue) + return defaultValue || ''; + else + return this._cachedValue; + }, + + resetCache: function() { + this.setCache(''); + } +}); diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl new file mode 100644 index 00000000..1084a4a6 --- /dev/null +++ b/client/components/forms/forms.styl @@ -0,0 +1,636 @@ +@import 'nib' + +textarea, +input:not([type=file]), +button + box-sizing: border-box + -webkit-appearance: none + background-color: #ebebeb + border: 1px solid #ccc + border-radius: 3px + display: block + margin-bottom: 12px + min-height: 34px + padding: 7px + + &.full + width: 100% + + &.input-error + background-color: #ece9e9 + border-color: #ba1212 + + &:focus + outline: 0 + +input[type="file"] + margin-bottom: 16px + +input[type="radio"] + -webkit-appearance: radio + min-height: inherit + +input[type="checkbox"] + -webkit-appearance: checkbox + margin-right: 4px + +input[type="text"], +input[type="password"], +input[type="email"] + transition: background 85ms ease-in, + border-color 85ms ease-in + width: 250px + + &.inline-input + background: none + border: 0 + margin: 0 + padding: 2px + min-height: 0 + height: 18px + width: 200px + +input[type="email"]:invalid + box-shadow: none + +input[type="text"], +input[type="password"], +input[type="email"], +textarea + + &:hover + border-color: #999 + + &.input-error + border-color: #ba1212 + + &:focus + background: #fff + border-color: #318ec4 + box-shadow: 0 0 2px #318ec4 + + &.input-error + background-color: #f8f7f7 + border-color: #ba1212 + box-shadow: 0 0 2px #d11515 + + &:disabled + background-color: #dcdcdc + border-color: #bfbfbf + color: #8c8c8c + -webkit-touch-callout: none + user-select: none + +select + max-height: 300px + width: 256px + margin-bottom: 8px + +option[disabled] + color: #8c8c8c + +textarea + height: 150px + transition: background 85ms ease-in, + border-color 85ms ease-in + resize: vertical + width: 100% + +.button + border-radius: 3px + text-decoration: none + position: relative + +input[type="submit"], +button + background: #cfcfcf + background: linear-gradient(#cfcfcf, #c2c2c2) + border: none + box-shadow: 0 1px 0 #8c8c8c + cursor: pointer + display: inline-block + font-weight: 700 + line-height: 22px + margin: 8px 4px 0 0 + padding: 7px 20px + text-align: center + + .wide + padding-left: 30px + padding-right: 30px + + &:hover, + &:focus + background: #c2c2c2 + background: linear-gradient(#c2c2c2, #b5b5b5) + + &:active + background: #b5b5b5 + background: linear-gradient(#b5b5b5, #a8a8a8) + box-shadow: inset 0 3px 6px rgba(0, 0, 0, .1) + + &:hover, + &:focus, + &:active + background: #e6e6e6 + background: linear-gradient(#e6e6e6, #e6e6e6) + + &.primary + background: #005377 + box-shadow: 0 1px 0 #4d4d4d + color: white + + &:hover, + &:focus + background: #004766 + + &:active + background: #01628C + + &.negate + &:hover, + &:focus + background: #990f0f + background: linear-gradient(#990f0f, #7d0c0c) + box-shadow: 0 1px 0 #4d4d4d + color: #fff + + &:active + background: #7d0c0c + box-shadow: 0 1px 0 #4d4d4d + color: #fff + +input[type="submit"].disabled, +input[type="submit"]:disabled, +input[type="button"].disabled, +button.disabled, +.button.disabled + + &, + &:hover, + &:active + background: #cfcfcf + cursor: default + box-shadow: none + color: #a8a8a8 + +fieldset + border: 1px solid #bfbfbf + padding: 15px + margin-bottom: 15px + +input[type="hidden"] + display: none + +input[type="checkbox"], +input[type="radio"] + display: inline + +.radio-div, +.check-div + display: block + margin: 0 0 4px 20px + min-height: 20px + position: relative + + input + left: -18px + min-height: 0 + margin: 0 + padding: 0 + position: absolute + top: 2px + + label + font-weight: 400 + +label + display: block + font-weight: 700 + margin-bottom: 4px + + &.form-error + color: #ba1212 + +input, +textarea + &::-webkit-input-placeholder, + &::-moz-placeholder + color: #8c8c8c + +.edit-controls, +.add-controls + margin-top: 0 + + button[type=submit] + float: left + height: 32px + margin-top: -2px + padding-top: 5px + padding-bottom: 5px + + i.fa.fa-times + font-size: 20px + + .option + border-color: transparent + border-radius: 3px + color: #8c8c8c + display: block + float: right + height: 30px + line-height: 30px + padding: 0 8px + margin: 0 2px + + &:hover + background-color: #dbdbdb + color: #4d4d4d + + &:active + background-color: #ccc + +.button-link + background: #fff + background: linear-gradient(#fff, #f5f5f5) + border-radius: 3px + box-sizing: border-box + user-select: none + border: 1px solid #e3e3e3 + border-bottom-color: #c2c2c2 + cursor: pointer + display: block + font-weight: 700 + height: 34px + margin-top: 6px + max-width: 300px + padding: 7px + position: relative + text-decoration: none + overflow: ellipsis + + .on + background: #48b512 + background: linear-gradient(#48b512, #3d990f) + border-radius: 3px + color: #fff + display: none + font-size: 12px + font-weight: 700 + height: 17px + line-height: @height + margin: 0 + padding: 2px 4px + position: absolute + right: 5px + top: 5px + text-align: center + + &.is-on + padding-right: 30px + max-width: 196px + + .on + display: block + + &.inline + color: #666 + padding: 2px 14px + margin-left: 4px + + &.setting + height: 52px + float: left + position: relative + margin-top: 0 + + &.disabled + background: #fff + border-color: #e9e9e9 + color: #8c8c8c + cursor: default + + select + display: none + + &:hover .label + color: #8c8c8c + + &, + &:hover, + &:active, + &.primary, + &.primary:hover, + &.primary:active + background: #cfcfcf + border-color: #c2c2c2 + border-bottom-color: #b5b5b5 + cursor: default + box-shadow: none + color: #a8a8a8 + + .label + color: #8c8c8c + display: block + font-size: 12px + line-height: 14px + margin-bottom: 0 + + &:hover .label + color: #eee + + .value + display: block + font-size: 18px + line-height: 24px + overflow: hidden + text-overflow: ellipsis + + label + display: none + + select + border: none + cursor: pointer + height: 50px + left: 0 + margin: 0 + opacity: 0 + position: absolute + top: 0 + z-index: 2 + width: 100% + + &:hover + background: #318ec4 + background: linear-gradient(#318ec4, #2b7cab) + border-color: #2e85b8 + color: #fff + + .on + background-image: none + background-color: rgba(255, 255, 255, .3) + border-color: transparent + + .icon-sm + color: #fff + + &:active + background: #2e85b8 + background: linear-gradient(#2e85b8, #28739f) + border-color: #2b7cab + color: #fff + + .button-link.negate + + &:hover + background: #990f0f + background: linear-gradient(#990f0f, #7d0c0c) + border-color: @background + + &:active + background: #7d0c0c + border-color: #990f0f + + + &.primary + background: #48b512 + background: linear-gradient(#48b512, #3d990f) + border: 1px solid + border-color: #3d990f + color: #fff + + &:hover + background: #3d990f + background: linear-gradient(#3d990f, #327d0c) + border-color: #3d990f + + &.danger + background: #ba1212 + background: linear-gradient(#ba1212, #8b0e0e) + border: 1px solid + border-color: #a21010 + color: #fff + + &:hover + background: #a21010 + background: linear-gradient(#a21010, #740b0b) + border-color: #8b0e0e + +button + + &.quiet-button, + &.loud-text-button + background: none + text-align: left + line-height: normal + border: none + box-shadow: none + + &:active + color: #4d4d4d + background: #d3d3d3 + box-shadow: none + + &.quiet-button + font-weight: 400 + text-decoration: underline + + &.loud-text-button + width: 100% + + &:hover + color: #111 + +.emphasis-button, +.quiet-button + border-radius: 3px + user-select: none + color: #8c8c8c + display: block + margin: 2px 0 + padding: 6px 8px + position: relative + + &.w-img + padding-left: 28px + + .icon-sm + left: 6px + position: absolute + top: 6px + + &:hover + color: #4d4d4d + background: #dcdcdc + + &:active + color: #4d4d4d + background: #d3d3d3 + +.quiet-button-large + padding: 16px 24px + +.emphasis-button + color: #74663e + background: #ecdfbb + + &:hover + color: #53492d + background: #e7d6a7 + + &:active + color: #53492d + background: #e1cc93 + +.big-link + border-radius: 3px + display: block + margin: 6px 0 6px 40px + padding: 11px + position: relative + text-decoration: none + font-size: 16px + line-height: 20px + + .text + text-decoration: underline + + &:hover + background: #dcdcdc + + &.options + padding-right: 41px + + .option + height: 30px + width: @height + position: absolute + right: 6px + top: 6px + + &.none + color: #8c8c8c + text-decoration: none + + &:hover + background: transparent + + &.avatar-changer + padding-right: 51px + + .member + border: 1px solid #ccc + border-radius: 3px + height: 40px + width: @height + position: absolute + right: 0 + top: 0 + + .member-avatar + height: 40px + width: @height + + .member-initials + font-size: 16px + height: 40px + line-height: @height + max-height: @height + +.show-more + border-radius: 3px + color: #8c8c8c + display: block + padding: 16px 8px 16px 40px + margin: 8px 0 + + &:hover + background: #dcdcdc + text-decoration: underline + + &.compact + padding: 12px 8px + margin: 8px 0 0 + text-align: center + +.board-widget .show-more + padding: 12px 8px 12px 40px + +.uploader + clear: both + cursor: pointer + position: relative + height: 34px + width: 100% + + .realfile + cursor: pointer + height: 34px + line-height: @height + position: absolute + top: 0 + left: 0 + width: 100% + z-index: 2 + font-size: 23px + + input[type="file"] + cursor: pointer + height: 34px + line-height: @height + margin: 0 + opacity: 0 + padding: 0 + width: 100% + z-index: 2 + font-size: 23px + + &:hover .fakefile + background: #318ec4 + background: linear-gradient(#318ec4, #2b7cab) + border-color: #2e85b8 + color: #fff + +.form-grid + display: flex + flex-wrap: wrap + width: 100% + +.form-grid-child + flex: 1 + margin: 0 0 8px + +.form-grid-child-full + flex: 1 1 100% + +.form-grid-child-threequarters + flex: 3 + margin-right: 8px + +.form-grid-child-twothirds + flex: 2 + margin-right: 8px + +.dropdown-menu + border-radius: 2px + // padding-bottom: 3px + overflow: hidden + + li + border-top: none + + a + padding: 4px 12px 4px 8px + + img + width: 18px + height: @width + margin-right: 5px + vertical-align: middle + + &.active + background: #005377 + + a + color: white diff --git a/client/components/forms/inlinedform.jade b/client/components/forms/inlinedform.jade new file mode 100644 index 00000000..5ad9039e --- /dev/null +++ b/client/components/forms/inlinedform.jade @@ -0,0 +1,6 @@ +template(name='inlinedForm') + if isOpen.get + form(id=id class=classNames) + +Template.contentBlock + else + +Template.elseBlock diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js new file mode 100644 index 00000000..2e2b2eba --- /dev/null +++ b/client/components/forms/inlinedform.js @@ -0,0 +1,93 @@ +// A inlined form is used to provide a quick edition of single field for a given +// document. Clicking on a edit button should display the form to edit the field +// value. The form can then be submited, or just closed. +// +// When the form is closed we save non-submitted values in memory to avoid any +// data loss. +// +// Usage: +// +// +inlineForm +// // the content when the form is open +// else +// // the content when the form is close (optional) + +// We can only have one inlined form element opened at a time +// XXX Could we avoid using a global here ? This is used in Mousetrap +// keyboard.js +currentlyOpenedForm = new ReactiveVar(null); + +BlazeComponent.extendComponent({ + template: function() { + return 'inlinedForm'; + }, + + mixins: function() { + return [Mixins.CachedValue]; + }, + + onCreated: function() { + this.isOpen = new ReactiveVar(false); + }, + + open: function() { + // Close currently opened form, if any + if (currentlyOpenedForm.get() !== null) { + currentlyOpenedForm.get().close(); + } + this.isOpen.set(true); + currentlyOpenedForm.set(this); + }, + + close: function() { + this.saveValue(); + this.isOpen.set(false); + currentlyOpenedForm.set(null); + }, + + getValue: function() { + return this.isOpen.get() && this.find('textarea,input[type=text]').value; + }, + + saveValue: function() { + this.callFirstWith(this, 'setCache', this.getValue()); + }, + + events: function() { + return [{ + 'click .js-close-inlined-form': this.close, + 'click .js-open-inlined-form': this.open, + + // Close the inlined form by pressing escape. + // + // Keydown (and not keypress) in necessary here because the `keyCode` + // property is consistent in all browsers, (there is not keyCode for the + // `keypress` event in firefox) + 'keydown form input, keydown form textarea': function(evt) { + if (evt.keyCode === 27) { + evt.preventDefault(); + this.close(); + } + }, + + // Pressing Ctrl+Enter should submit the form + 'keydown form textarea': function(evt) { + if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { + $(evt.currentTarget).parents('form:first').submit(); + } + }, + + // Close the inlined form when after its submission + submit: function() { + var self = this; + // XXX Swith to an arrow function here when we'll have ES6 + if (this.currentData().autoclose !== false) { + Tracker.afterFlush(function() { + self.close(); + self.callFirstWith(self, 'resetCache'); + }); + } + } + }]; + } +}).register('inlinedForm'); diff --git a/client/components/lists/body.jade b/client/components/lists/body.jade new file mode 100644 index 00000000..0e8efeeb --- /dev/null +++ b/client/components/lists/body.jade @@ -0,0 +1,50 @@ +template(name="listBody") + .minicards.clearfix.js-minicards + if cards.count + +inlinedForm(autoclose=false position="top") + +addCardForm + each cards + .minicard.card.js-minicard.js-member-droppable( + class="{{#if isSelected}}is-selected{{/if}}") + a.minicard-details.clearfix.show(href=absoluteUrl) + if cover + .minicard-cover.js-card-cover(style="background-image: url({{cover.url}});") + if labels + .minicard-labels + each labels + .minicard-label(class="card-label-{{color}}" title="{{name}}") + .minicard-title= title + if members + .minicard-members.js-minicard-members + each members + +userAvatar(userId=this size="small" cardId="{{../_id}}") + .badges + if comments.count + .badge(title="{{_ 'card-comments-title' comments.count }}") + span.badge-icon.icon-sm.fa.fa-comment-o + .badge-text= comments.count + if description + .badge.badge-state-image-only(title=description) + span.badge-icon.icon-sm.fa.fa-align-left + if attachments.count + .badge + span.badge-icon.icon-sm.fa.fa-paperclip + span.badge-text= attachments.count + if currentUser.isBoardMember + +inlinedForm(autoclose=false position="bottom") + +addCardForm + else + a.open-card-composer.js-open-inlined-form + i.fa.fa-plus + | {{_ 'add-card'}} + +template(name="addCardForm") + .minicard.js-composer + .minicard-labels.js-minicard-composer-labels + .minicard-details.clearfix + textarea.minicard-composer-textarea.js-card-title(autofocus) + = getCache + .minicard-members.js-minicard-composer-members + .add-controls.clearfix + button.primary.confirm(type="submit") {{_ 'add'}} + a.fa.fa-times.dark-hover.cancel.js-close-inlined-form diff --git a/client/components/lists/body.js b/client/components/lists/body.js new file mode 100644 index 00000000..fa6ec096 --- /dev/null +++ b/client/components/lists/body.js @@ -0,0 +1,73 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'listBody'; + }, + + isSelected: function() { + return Session.equals('currentCard', this.currentData()._id); + }, + + addCard: function(evt) { + evt.preventDefault(); + var textarea = $(evt.currentTarget).find('textarea'); + var title = textarea.val(); + var position = this.currentData().position; + var sortIndex; + if (position === 'top') { + sortIndex = Utils.getSortIndex(null, this.find('.js-minicard:first')); + } else if (position === 'bottom') { + sortIndex = Utils.getSortIndex(this.find('.js-minicard:last'), null); + } + + // Clear the form in-memory cache + // var inputCacheKey = "addCard-" + this.listId; + // InputsCache.set(inputCacheKey, ''); + + // title trim if not empty then + if ($.trim(title)) { + Cards.insert({ + title: title, + listId: this.data()._id, + boardId: this.data().board()._id, + sort: sortIndex + }, function(err, _id) { + // In case the filter is active we need to add the newly + // inserted card in the list of exceptions -- cards that are + // not filtered. Otherwise the card will disappear instantly. + // See https://github.com/libreboard/libreboard/issues/80 + Filter.addException(_id); + }); + + // We keep the form opened, empty it, and scroll to it. + textarea.val('').focus(); + Utils.Scroll(this.find('.js-minicards')).top(1000, true); + } + }, + + events: function() { + return [{ + submit: this.addCard, + 'keydown form textarea': function(evt) { + // Pressing Enter should submit the card + if (evt.keyCode === 13) { + evt.preventDefault(); + $(evt.currentTarget).parents('form:first').submit(); + + // Pressing Tab should open the form of the next column, and Maj+Tab go + // in the reverse order + } else if (evt.keyCode === 9) { + evt.preventDefault(); + var isReverse = evt.shiftKey; + var list = $('#js-list-' + this.data()._id); + var nextList = list[isReverse ? 'prev' : 'next']('.js-list').get(0) || + $('.js-list:' + (isReverse ? 'last' : 'first')).get(0); + var nextListComponent = BlazeComponent.getComponentForElement(nextList); + + // XXX Get the real position + var position = 'bottom'; + nextListComponent.openForm({position: position}); + } + } + }]; + } +}).register('listBody'); diff --git a/client/components/lists/events.js b/client/components/lists/events.js new file mode 100644 index 00000000..f636de75 --- /dev/null +++ b/client/components/lists/events.js @@ -0,0 +1,16 @@ +Template.addlistForm.events({ + submit: function(event, t) { + event.preventDefault(); + var title = t.find('.list-name-input'); + if ($.trim(title.value)) { + Lists.insert({ + title: title.value, + boardId: Session.get('currentBoard'), + sort: $('.list').length + }); + + Utils.Scroll('.js-lists').left(270, true); + title.value = ''; + } + } +}); diff --git a/client/components/lists/header.jade b/client/components/lists/header.jade new file mode 100644 index 00000000..5196af5d --- /dev/null +++ b/client/components/lists/header.jade @@ -0,0 +1,13 @@ +template(name="listHeader") + .list-header.js-list-header + +inlinedForm + +editListTitleForm + else + h2.list-header-name.js-open-inlined-form= title + a.list-header-menu-icon.fa.fa-bars.js-open-list-menu + +template(name="editListTitleForm") + input.field.single-line(type="text" value="{{getCache title}}" autofocus) + .edit-controls.clearfix + input.primary.confirm(type="submit" value="{{_ 'save'}}") + a.fa.fa-times.js-close-inlined-form diff --git a/client/components/lists/header.js b/client/components/lists/header.js new file mode 100644 index 00000000..014cfd80 --- /dev/null +++ b/client/components/lists/header.js @@ -0,0 +1,25 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'listHeader'; + }, + + editTitle: function(evt) { + evt.preventDefault(); + var form = this.componentChildren('inlinedForm')[0]; + var newTitle = form.getValue(); + if ($.trim(newTitle)) { + Lists.update(this.currentData()._id, { + $set: { + title: newTitle + } + }); + } + }, + + events: function() { + return [{ + 'click .js-open-list-menu': Popup.open('listAction'), + submit: this.editTitle + }]; + } +}).register('listHeader'); diff --git a/client/components/lists/main.jade b/client/components/lists/main.jade new file mode 100644 index 00000000..dd4bb49a --- /dev/null +++ b/client/components/lists/main.jade @@ -0,0 +1,5 @@ +template(name='list') + .list.js-list(id="js-list-{{_id}}") + .list-wrapper + +listHeader + +listBody diff --git a/client/components/lists/main.js b/client/components/lists/main.js new file mode 100644 index 00000000..3d458055 --- /dev/null +++ b/client/components/lists/main.js @@ -0,0 +1,81 @@ +ListComponent = BlazeComponent.extendComponent({ + template: function() { + return 'list'; + }, + + openForm: function(options) { + options = options || {}; + options.position = options.position || 'top'; + + var listComponent = this.componentChildren('listBody')[0]; + var forms = listComponent.componentChildren('inlinedForm'); + + if (options.position === 'top') { + forms[0].open(); + } else { + forms[forms.length - 1].open(); + } + }, + + // XXX The jQuery UI sortable plugin is far from ideal here. First we include + // all jQuery components but only use one. Second, it modifies the DOM itself, + // resulting in Blaze abandoning reactive update of the nodes that have been + // moved which result in bugs if multiple users use the board in real time. + // I tried sortable:sortable but that was not better. Should we “simply” write + // the drag&drop code ourselves? + onRendered: function() { + if (Meteor.user().isBoardMember()) { + var $cards = this.$('.js-minicards'); + $cards.sortable({ + connectWith: ".js-minicards", + tolerance: 'pointer', + appendTo: '.js-lists', + helper: "clone", + items: '.js-minicard:not(.placeholder, .hide, .js-composer)', + placeholder: 'minicard placeholder', + start: function (event, ui) { + $('.minicard.placeholder').height(ui.item.height()); + Popup.close(); + }, + stop: function(event, ui) { + // To attribute the new index number, we need to get the dom element of + // the previous and the following card -- if any. + var cardDomElement = ui.item.get(0); + var prevCardDomElement = ui.item.prev('.js-minicard').get(0); + var nextCardDomElement = ui.item.next('.js-minicard').get(0); + var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement); + var cardId = Blaze.getData(cardDomElement)._id; + var listId = Blaze.getData(ui.item.parents('.list').get(0))._id; + Cards.update(cardId, { + $set: { + listId: listId, + sort: sort + } + }); + } + }).disableSelection(); + + Utils.liveEvent('mouseover', function($el) { + $el.find('.js-member-droppable').droppable({ + hoverClass: "draggable-hover-card", + accept: '.js-member', + drop: function(event, ui) { + var memberId = Blaze.getData(ui.draggable.get(0)).userId; + var cardId = Blaze.getData(this)._id; + Cards.update(cardId, {$addToSet: {members: memberId}}); + } + }); + + $el.find('.js-member-droppable').droppable({ + hoverClass: "draggable-hover-card", + accept: '.js-label', + drop: function(event, ui) { + var labelId = Blaze.getData(ui.draggable.get(0))._id; + var cardId = Blaze.getData(this)._id; + Cards.update(cardId, {$addToSet: {labelIds: labelId}}); + } + }); + }); + } + } +}).register('list'); diff --git a/client/components/lists/main.styl b/client/components/lists/main.styl new file mode 100644 index 00000000..18484174 --- /dev/null +++ b/client/components/lists/main.styl @@ -0,0 +1,136 @@ +@import 'nib' + +.list + box-sizing: border-box + display: flex + flex-direction: column + flex: 0 0 270px + position: relative + // Even if this background color is the same as the body we can't leave it + // transparent, because that won't work during a list drag. + background: darken(white, 10%) + height: 100% + border-right: 1px solid darken(white, 17%) + border-left: 1px solid darken(white, 4%) + padding: 12px 7px 5px + overflow-y: auto + + &:first-child + margin-left: 5px + border-left: none + + &:last-child + margin-right: 5px + border-right: none + + &.editable + cursor: grab + + .list-wrapper + cursor: default + + &.add-list + &.fade + opacity: 0 + + .list-name-input + background: rgba(0, 0, 0, .05) + border-color: #aaa + box-shadow: inset 0 1px 8px rgba(0, 0, 0, .15) + display: block + margin: 0 + transition: margin 85ms ease-in, + background 85ms ease-in + width: 100% + + .edit-controls + height: 32px + transition: margin 85ms ease-in, + height 85ms ease-in + overflow: hidden + margin: 4px 0 0 + + input[type=submit] + margin-top: 0 + min-height: 30px + height: 30px + +.list-header + flex: 0 0 auto + padding: 10px 26px 4px 6px + position: relative + min-height: 20px + + .list-header-name + display: inline + font-size: 16px + line-height: 17px + margin: 0 + font-weight: bold + min-height: 9px + min-width: 30px + overflow: hidden + text-overflow: ellipsis + word-wrap: break-word + + .list-header-menu-icon + background-clip: content-box + background-origin: content-box + padding: 6px 8px + position: absolute + top: 3px + right: -5px + color: #a6a6a6 + + .list-header-num-cards + color: #8c8c8c + margin: 0 + +.minicards + // flex: 1 1 auto + overflow-y: auto + overflow-x: hidden + padding: 4px 4px 1px + z-index: 1 + height: 100% + + &::-webkit-scrollbar-button + display: block + height: 4px + +.open-card-composer + border-top-left-radius: 0 + border-top-right-radius: 0 + border-bottom-right-radius: 3px + border-bottom-left-radius: 3px + color: #8c8c8c + display: block + // flex: 0 0 auto + margin: 2px -3px -3px + padding: 7px 10px + position: relative + text-decoration: none + + &:hover + background: #c3c3c3 + color: #222 + text-decoration: underline + + + &::selection + background: transparent + +.list.placeholder + background-color: rgba(0, 0, 0, .2) + border-color: transparent + box-shadow: none + height: 100px + +.list.ui-sortable-helper + cursor: grabbing + box-shadow: -2px 2px 8px rgba(0, 0, 0, .3), 0 0 1px rgba(0, 0, 0, .5) + transform: rotate(4deg) + + +.list.ui-sortable-helper .list-header-menu-icon + display: none diff --git a/client/components/lists/menu.jade b/client/components/lists/menu.jade new file mode 100644 index 00000000..ff7820a4 --- /dev/null +++ b/client/components/lists/menu.jade @@ -0,0 +1,28 @@ +template(name="listActionPopup") + ul.pop-over-list + li: a.js-add-card {{_ 'add-card'}} + li: a.highlight-icon.js-list-subscribe {{_ 'subscribe'}} + if cards.count + hr + ul.pop-over-list + li: a.js-move-cards {{_ 'list-move-cards'}} + li: a.js-archive-cards {{_ 'list-archive-cards'}} + hr + ul.pop-over-list + li: a.js-close-list {{_ 'archive-list'}} + +template(name="listMoveCardsPopup") + +boardLists + +template(name="boardLists") + ul.pop-over-list + each currentBoard.lists + li + if($eq ../_id _id) + a.disabled {{title}} ({{_ 'current'}}) + else + a.js-select-list= title + +template(name="listArchiveCardsPopup") + p {{_ 'list-archive-cards-pop'}} + input.js-confirm.negate.full(type="submit" value="{{_ 'archive-all'}}") diff --git a/client/components/lists/menu.js b/client/components/lists/menu.js new file mode 100644 index 00000000..ef08cf76 --- /dev/null +++ b/client/components/lists/menu.js @@ -0,0 +1,46 @@ +Template.listActionPopup.events({ + 'click .js-add-card': function() { + // XXX We need a better API and architecture here. See + // https://github.com/peerlibrary/meteor-blaze-components/issues/19 + var listDom = document.getElementById('js-list-' + this._id); + var listComponent = Blaze.getView(listDom).templateInstance().get('component'); + listComponent.openForm(); + Popup.close(); + }, + 'click .js-list-subscribe': function() {}, + 'click .js-move-cards': Popup.open('listMoveCards'), + 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { + Cards.find({listId: this._id}).forEach(function(card) { + Cards.update(card._id, { + $set: { + archived: true + } + }); + }); + Popup.close(); + }), + 'click .js-close-list': function(evt) { + evt.preventDefault(); + Lists.update(this._id, { + $set: { + archived: true + } + }); + Popup.close(); + } +}); + +Template.listMoveCardsPopup.events({ + 'click .js-select-list': function() { + var fromList = Template.parentData(2).data._id; + var toList = this._id; + Cards.find({listId: fromList}).forEach(function(card) { + Cards.update(card._id, { + $set: { + listId: toList + } + }); + }); + Popup.close(); + } +}); diff --git a/client/components/main/events.js b/client/components/main/events.js new file mode 100644 index 00000000..beb90c5e --- /dev/null +++ b/client/components/main/events.js @@ -0,0 +1,8 @@ +Template.editor.events({ + // Pressing Ctrl+Enter should submit the form. + 'keydown textarea': function(event) { + if (event.keyCode === 13 && (event.metaKey || event.ctrlKey)) { + $(event.currentTarget).parents('form:first').submit(); + } + } +}); diff --git a/client/components/main/header.jade b/client/components/main/header.jade new file mode 100644 index 00000000..588c9b6e --- /dev/null +++ b/client/components/main/header.jade @@ -0,0 +1,40 @@ +template(name="header") + #header(class=currentBoard.colorClass) + //- + If the user is connected we display a small "quick-access" top bar that + list all starred boards with a link to go there. This is inspired by the + Reddit "subreddit" bar. + The first link goes to the boards page. + if currentUser + #header-quick-access + ul + li + +linkTo(route="Boards") + span.fa.fa-home + | All boards + each currentUser.starredBoards + li.separator - + li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}") + +linkTo(route="Board" data=this) + = title + else + li.current Star a board to add a shortcut in this bar. + + li + a.js-create-board + i.fa.fa-plus(title="Create a new board") + + +headerUserBar + + //- + The main bar is a colorful bar that provide all the meta-data for the + current page. This bar is contextual based. + If the user is not connected we display "sign in" and "log in" buttons. + #header-main-bar + if $.Session.get 'currentBoard' + +headerBoard + else + +headerTitle + +template(name="headerTitle") + h1 LibreBoard diff --git a/client/components/main/header.js b/client/components/main/header.js new file mode 100644 index 00000000..2a545309 --- /dev/null +++ b/client/components/main/header.js @@ -0,0 +1,10 @@ +Template.header.helpers({ + // Reactively set the color of the page from the color of the current board. + headerTemplate: function() { + return 'headerBoard'; + } +}); + +Template.header.events({ + 'click .js-create-board': Popup.open('createBoard') +}); diff --git a/client/components/main/header.styl b/client/components/main/header.styl new file mode 100644 index 00000000..1177d930 --- /dev/null +++ b/client/components/main/header.styl @@ -0,0 +1,266 @@ +@import 'nib' + +global-reset() + +#header + color: white + transition: background-color 0.4s + background: #27AE60 + + #header-quick-access + background-color: rgba(0, 0, 0, 0.2) + padding: 4px 10px + height: 16px + font-size: 12px + display: flex + + ul li, #header-user-bar + color: darken(white, 17%) + + a + color: inherit + text-decoration: none + + &:hover + color: white + + ul + flex: 1 + transition: opacity 0.2s + margin-left: 5px + + li + display: block + float: left + width: auto + color: darken(white, 15%) + padding: 0 4px 1px 4px + + &.separator + padding: 0 2px 1px 2px + + &.current + font-style: italic + + &:first-child .fa-home + margin-right: 5px + + #header-main-bar + height: 30px + padding: 8px + + h1 + font-size: 19px + line-height: 1.7em + margin: 0 20px 0 10px + float: left + + &.header-board-menu + cursor: pointer + + .fa-angle-down + font-size: 0.8em + // line-height: 1.1em + margin-left: 5px + + .board-header-starred .fa + color: yellow + + .board-header-members + float: right + + .member + display: block + width: 32px + height: @width + + .add-board-member + color: white + display: flex + align-items: center + justify-content: center + border: 1px solid white + height: 32px - 2px + width: @height + + i.fa-plus + margin-top: 2px + + .header-btn:last-child + margin-right: 0 + + + +// #header { +// background: #138871; +// height: 30px; +// overflow: hidden; +// padding: 5px; +// position: relative; +// z-index: 10; +// } + +// .header-logo { +// bottom: 0; +// display: block; +// height: 25px; +// left: 50%; +// position: absolute; +// top: 8px; +// width: 80px; +// margin-left: - @width/2; +// text-align: center; +// z-index: 2; +// opacity: .5; +// transition: opacity ease-in 85ms; +// color: white; +// font-size: 22px; +// text-decoration: none; +// background-image: url('/logos/white_logo.png'); + +// &:hover { +// opacity: .8; +// color: white; +// } +// } + +// .header-btn.header-btn-feedback { +// background: rgba(255, 255, 255, .1); +// background: linear-gradient(to bottom, rgba(255, 255, 255, .1) 0, rgba(255, 255, 255, .05) 100%); +// padding-left: 22px; +// margin-right: 16px; + +// .header-btn-icon { +// top: 1px; +// } +// } + +.header-btn { + border-radius: 3px; + user-select: none; + background: rgba(255, 255, 255, .3); + background: linear-gradient(to bottom, rgba(255, 255, 255, .3) 0, rgba(255, 255, 255, .2) 100%); + color: #f3f3f3; + display: block; + float: left; + font-weight: 700; + height: 30px; + line-height: 30px; + padding: 0 10px; + position: relative; + margin-right: 8px; + min-width: 30px; + text-decoration: none; + cursor: pointer; + + .header-btn-icon { + font-size: 16px; + line-height: 28px; + position: absolute; + top: 0; + left: 0; + } + + &.new-notifications { + background: #ba1212; + + &:hover { + background: #d11515; + } + } + + &.header-member .member { + margin: 0; + border-top-left-radius: 3px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; + + &:hover .member-avatar { + opacity: 1; + } + } + + &:hover { + background: rgba(255, 255, 255, .4); + background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%); + color: #fff; + + .header-btn-count { + background: #d11515; + } + } + + &:active { + background: rgba(255, 255, 255, .4); + background: linear-gradient(to bottom, rgba(255, 255, 255, .4) 0, rgba(255, 255, 255, .3) 100%); + } + + &.upgrade { + margin-right: 16px; + + .icon-sm { + padding: 6px 2px 6px 4px; + } + } + + &.upgrade, + &.header-boards { + padding-left: 4px; + } + + &.header-boards { + padding-right: 4px; + } + + &.header-login, + &.header-signup { + padding: 0 12px; + } + + &.header-signup { + background: #48b512; + background: linear-gradient(to bottom, #48b512 0, #3d990f 100%); + + &:hover { + background: #3d990f; + background: linear-gradient(to bottom, #3d990f 0, #327d0c 100%); + } + + &:active { + background: #327d0c; + } + } + + &.header-go-to-boards { + padding: 0 8px 0 38px; + } + + &.header-go-to-boards .member { + border-top-left-radius: 3px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; + position: absolute; + left: 0; + } +} + +// .header-btn-text { +// padding: 0 8px; +// } + +// .header-notification-list ul { +// margin-top: 8px; +// } + +// .header-notification-list .action-comment { +// max-height: 250px; +// overflow-y: auto; +// } + +// .header-user { +// position: absolute; +// top: 5px; +// right: 0; +// } diff --git a/client/components/main/helpers.js b/client/components/main/helpers.js new file mode 100644 index 00000000..7ad602f1 --- /dev/null +++ b/client/components/main/helpers.js @@ -0,0 +1,63 @@ +var Helpers = { + error: function() { + return Session.get('error'); + }, + + toLowerCase: function(text) { + return text && text.toLowerCase(); + }, + + toUpperCase: function(text) { + return text && text.toUpperCase(); + }, + + firstChar: function(text) { + return text && text[0].toUpperCase(); + }, + + session: function(prop) { + return Session.get(prop); + }, + + getUser: function(userId) { + return Users.findOne(userId); + } +}; + +// Register all Helpers +_.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); }); + +// XXX I believe we should compute a HTML rendered field on the server that +// would handle markdown, emojies and user mentions. We can simply have two +// fields, one source, and one compiled version (in HTML) and send only the +// compiled version to most users -- who don't need to edit. +// In the meantime, all the transformation are done on the client using the +// Blaze API. +var at = HTML.CharRef({html: '@', str: '@'}); +Blaze.Template.registerHelper('mentions', new Template('mentions', function() { + var view = this; + var content = Blaze.toHTML(view.templateContentBlock); + var currentBoard = Session.get('currentBoard'); + var knowedUsers = _.map(currentBoard.members, function(member) { + member.username = Users.findOne(member.userId).username; + return member; + }); + + var mentionRegex = /\B@(\w*)/gi; + var currentMention, knowedUser, href, linkClass, linkValue, link; + while (currentMention = mentionRegex.exec(content)) { + + knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] }); + if (! knowedUser) + continue; + + linkValue = [' ', at, knowedUser.username]; + href = Router.url('Profile', { username: knowedUser.username }); + linkClass = 'atMention' + (knowedUser.userId === Meteor.userId() ? ' me' : ''); + link = HTML.A({ href: href, 'class': linkClass }, linkValue); + + content = content.replace(currentMention[0], Blaze.toHTML(link)); + } + + return HTML.Raw(content); +})); diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade new file mode 100644 index 00000000..18df4d9e --- /dev/null +++ b/client/components/main/layouts.jade @@ -0,0 +1,17 @@ +head + title LibreBoard + meta(name="viewport" + content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0") + link(rel="shortcut icon" href="/favicon.png") + +template(name="userFormsLayout") + h1.at-form-landing-logo + img(src="/logo.png" title="LibreBoard") + +yield + +template(name="defaultLayout") + #surface + +header + #content + +yield + diff --git a/client/components/main/popup.js b/client/components/main/popup.js new file mode 100644 index 00000000..53695d10 --- /dev/null +++ b/client/components/main/popup.js @@ -0,0 +1,16 @@ +Popup.template.events({ + click: function(evt) { + if (evt.originalEvent) { + evt.originalEvent.clickInPopup = true; + } + }, + 'click .js-back-view': function() { + Popup.back(); + }, + 'click .js-close-popover': function() { + Popup.close(); + }, + 'click .js-confirm': function() { + this.__afterConfirmAction.call(this); + } +}); diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl new file mode 100644 index 00000000..8c9993af --- /dev/null +++ b/client/components/main/popup.styl @@ -0,0 +1,585 @@ +@import 'nib' + +.pop-over + background: #fff + border-radius: 3px + border: 1px solid #dbdbdb + border-bottom-color: #c2c2c2 + box-shadow: 0 1px 6px rgba(0, 0, 0, .3) + display: none + overflow: hidden + position: absolute + width: 300px + z-index: 99999 + margin-top: 5px + + hr + margin: 4px -10px + width: 275px + 2*10px + + input[type="text"], + input[type="email"], + input[type="password"] + margin: 4px 0 12px + width: 100% + + input[type="file"] + width: 240px + + select + width: 100% + margin-bottom: 14px + + textarea + height: 72px + margin: 4px 0 12px + width: 100% + + .empty + margin: 0 + + img + max-width: 270px + + .custom-image img + height: 18px + left: 9px + top: 9px + width: 18px + + .title + line-height: 32px + + .header + height: 36px + position: relative + margin-bottom: 8px + background: #F7F7F7 + border-bottom: 1px solid #dcdcdc + color: darken(white, 60%) + + .header-title + display: block + line-height: 32px + padding-top: 4px + margin: 0 10px + font-weight: bold + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + + .back-btn, .close-btn + &:hover .icon-sm + color: darken(white, 80%) + + .back-btn + padding: 10px + float: left + + .close-btn + padding: 10px 10px 10px 4px + position: absolute + top: 0 + right: 0 + + .content + overflow-x: hidden + overflow-y: auto + padding: 0 10px 10px + max-height: 550px + + .quiet + padding: 6px 6px 4px + + &.search-over + background: #f0f0f0 + min-height: 114px + + .header + display: none + + .content + padding: 8px 4px 8px 10px + margin-right: 8px + + &::-webkit-scrollbar-button + display: block + height: 4px + width: 4px + +.select-members-list + margin-bottom: 8px + +.pop-over-list + + &.navigable li.not-selectable>a:hover, + li.not-selectable>a:hover + color: #8c8c8c + cursor: default + + .icon-sm + color: #a6a6a6 + + li > a + cursor: pointer + display: block + font-weight: 700 + padding: 6px 10px + position: relative + margin: 0 -10px + text-decoration: none + + .item-name + display: block + width: auto + padding-right: 22px + + &:hover + background-color: #005377 + color: #fff + + .sub-name, + .quiet + color: #eee + + .unread-indicator + background: #fff + + .icon-sm + color: #fff + + .sub-name + clear: both + color: #8c8c8c + display: block + font-size: 12px + font-weight: 400 + line-height: 15px + margin-top: 4px + + &.current + background-color: #e2e6e9 + + .unread-indicator + background: #2e85b8 + background: linear-gradient(to bottom, #2e85b8 0, #2b7cab 100%) + border-radius: 7px + display: block + height: 14px + opacity: 0 + position: absolute + right: 16px + top: 8px + width: 14px + + &.any + opacity: 1 + + &:active + background-color: #2e85b8 + + &.disabled + color: #8c8c8c + cursor: default + + .vis-icon + opacity: .35 + + .icon-sm + color: #a6a6a6 + + &:hover + background: none + + .sub-name, + .quiet + color: #8c8c8c + + .icon-sm + color: #a6a6a6 + + &:active + background: none + + &.inset li > a + border-radius: 3px + margin: 0 + + .pop-over-list.checkable + + .icon-check + display: none + position: absolute + top: 6px + right: 12px + + li.active a + padding-right: 28px + + .icon-check + display: block + + &.left-check + + .icon-check + right: auto + left: 10px + + li a + padding-right: 10px + padding-left: 30px + + li.active a + padding-right: 10px + + &.normal-weight li>a + font-weight: 400 + + &.navigable + + li > a:hover + background-color: transparent + color: #4d4d4d + + .sub-name, + .quiet + color: #8c8c8c + + .icon-sm + color: #a6a6a6 + + li.selected > a + background-color: #005377 + color: #fff + + .sub-name, + .quiet + color: #eee + + li.selected > a + + &.current + background-color: #005377 + + .unread-indicator + background: #fff + + .icon-sm + color: #fff + + &:active + background-color: #005377 + +.pop-over.miniprofile + + .header + border-bottom-color: transparent + height: 30px + position: absolute + right: 0 + top: 0 + width: 60px + z-index: 1 + + .header-title + display: none + + .pop-over-list + padding-top: 8px + +.mini-profile-info + margin-top: 8px + min-height: 56px + position: relative + + .member-large + position: absolute + top: 2px + left: 2px + + .info + margin: 0 0 0 64px + word-wrap: break-word + + h3 a + text-decoration: none + + &:hover + text-decoration: underline + +.pop-over.avdetail .header + border-bottom-color: transparent + height: 20px + position: absolute + top: 8px + left: 8px + right: 8px + z-index: 0 + +.pop-over.avdetail .header-title + display: none + +.pop-over.avdetail .content + text-align: center + +.pop-over.avdetail .mem-info + margin: 2px 24px 8px + position: relative + z-index: 1 + width: 222px + +.pop-over.avdetail .mem-info h3 a + text-decoration: none + +.pop-over.avdetail .mem-info h3 a:hover + text-decoration: underline + +.pop-over-label-list li, +.pop-over-member-list li + + &.disabled a + cursor:default + + &:not(.disabled):hover a + background-color: #005377 + color: #fff + + +.pop-over-label-list, +.pop-over-member-list, +.pop-over-emoji-list, +.pop-over-card-list + li + a + border-radius: 3px + display: block + height: 30px + line-height: 30px + overflow: hidden + position: relative + text-overflow: ellipsis + text-decoration: none + white-space: nowrap + padding: 4px + margin-bottom: 2px + + &.multi-line + line-height: 16px + + .member + margin-right: 8px + + .card-label + float: left + height: 30px + margin: 0 8px 0 0 + padding: 0 + width: 30px + + .option, + .icon-check + background-clip: content-box + background-origin: content-box + padding: 11px + position: absolute + top: 0 + right: 0 + + .sub-name + font-size: 12px + + + &:last-child a + margin-bottom: 0 + + &.disabled + opacity: .5 + + &.active a, + &.selected a + background: none + color: #4d4d4d + cursor: default + + .quiet + color: #8c8c8c + + &.email-invite + + .member + display: none + + a + padding: 0 10px + + &.selected a + background-color: #005377 + color: #fff + + .quiet + color: #eee + + .card-label + border-radius: 3px + + .icon-check + color: #fff + + &.active a .icon-check + display: block + + &.unconfirmed a.name + line-height: 16px + + &.options li + + &.selected a + padding-right: 28px + + .option + display: block + opacity: .5 + + &:hover + opacity: 1 + + &.disabled.selected a + padding-right: 0 + + .option + display: none + + + &.no-option.selected a + padding-right: 6px + + .option + display: none + + &.collapsed + + &.checkable li.active a + padding-right: 0 + + li + float: left + margin: 0 3px 3px 0 + + a + padding: 0 + margin: 0 + width: 30px + + .member + opacity: .8 + + .full-name + display: none + + &.selected a .member, + &.active.selected a .member + border-color: #005377 + opacity: .9 + + &.active a + + .member + border-color: #2e85b8 + opacity: 1 + + .icon-check + border-radius: 3px + background-color: #2e85b8 + bottom: 0 + color: #fff + display: block + padding: 0 + right: 0 + top: auto + + &.checkable li.active a + padding-right: 28px + + &.filtered li + display: none + + &.matches-filter + display: block + + &.limited li.exceeds-limit + display: none + +.pop-over-emoji-list li > a + padding: 2px 4px + + .emoji + margin: 0 6px + +.pop-over-card-list li > a + padding: 2px 4px + +.login-signup-popover + padding: 15px + + .form-tabs + display: none + + h1 + margin-bottom: 15px + + p + margin: 8px 0 + + .form-parts-container + position: relative + + .active-box + position: absolute + top: 0 + background: #e2e2e2 + border: 1px solid #c9c9c9 + border-radius: 3px + z-index: 1 + height: 100% + width: 49% + transition-property: all + transition-duration: .4s + opacity: 1 + + &.start + opacity: 0 + left: 25% + + .signup-form, + .login-form + position: relative + box-sizing: border-box + padding: 20px + width: 50% + z-index: 2 + opacity: .3 + transition-property: opacity + transition-duration: .2s + + .active + opacity: 1 + + + .js-signup-form-pos + left: 0 + + .login-form + position: absolute + top: 0 + + .login-form .icon-google + position: absolute + left: 5px + top: 3px + + .login-form .button.google + padding-left: 40px + margin: 0 0 15px 0 + + .js-login-form-pos + left: 50% diff --git a/client/components/main/popup.tpl.jade b/client/components/main/popup.tpl.jade new file mode 100644 index 00000000..ba24db0a --- /dev/null +++ b/client/components/main/popup.tpl.jade @@ -0,0 +1,13 @@ +.pop-over.clearfix( + class="{{#unless title}}miniprofile{{/unless}}" + class=currentBoard.colorClass + style="display:block; left:{{offset.left}}px; top:{{offset.top}}px;") + .header.clearfix + if hasPopupParent + a.back-btn.js-back-view + i.fa.fa-chevron-left + span.header-title= title + a.close-btn.js-close-popover + i.fa.fa-times + .content.clearfix + +Template.dynamic(template=popupName data=dataContext) diff --git a/client/components/main/rendered.js b/client/components/main/rendered.js new file mode 100644 index 00000000..787e8225 --- /dev/null +++ b/client/components/main/rendered.js @@ -0,0 +1,40 @@ +Template.editor.rendered = function() { + this.$('textarea').textcomplete([ + // Emojies + { + match: /\B:([\-+\w]*)$/, + search: function(term, callback) { + callback($.map(Emoji.values, function(emoji) { + return emoji.indexOf(term) === 0 ? emoji : null; + })); + }, + template: function(value) { + var image = '<img src="' + Emoji.baseImagePath + value + '.png"></img>'; + return image + value; + }, + replace: function(value) { + return ':' + value + ':'; + }, + index: 1 + }, + + // User mentions + { + match: /\B@(\w*)$/, + search: function(term, callback) { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + callback($.map(currentBoard.members, function(member) { + var username = Users.findOne(member.userId).username; + return username.indexOf(term) === 0 ? username : null; + })); + }, + template: function(value) { + return value; + }, + replace: function(username) { + return '@' + username + ' '; + }, + index: 1 + } + ]); +}; diff --git a/client/components/main/router.js b/client/components/main/router.js new file mode 100644 index 00000000..bae832e8 --- /dev/null +++ b/client/components/main/router.js @@ -0,0 +1,5 @@ +Router.route('/', { + name: 'Home', + redirectLoggedInUsers: true, + authenticated: true +}); diff --git a/client/components/main/spinner.styl b/client/components/main/spinner.styl new file mode 100644 index 00000000..f4b8cc86 --- /dev/null +++ b/client/components/main/spinner.styl @@ -0,0 +1,45 @@ +/* + * From https://github.com/tobiasahlin/SpinKit + * + * Usage: + * + * <div class="sk-spinner sk-spinner-wave"> + * <div class="sk-rect1"></div> + * <div class="sk-rect2"></div> + * <div class="sk-rect3"></div> + * <div class="sk-rect4"></div> + * <div class="sk-rect5"></div> + * </div> + * + */ + +.sk-spinner-wave { + + &.sk-spinner { + width: 50px; + height: 50px; + margin: auto; + margin-top: 30vh; + text-align: center; + font-size: 10px; + } + + div { + background-color: #333; + height: 100%; + width: 6px; + display: inline-block; + + animation: sk-waveStretchDelay 1.2s infinite ease-in-out; + } + + .sk-rect2 { animation-delay: -1.1s } + .sk-rect3 { animation-delay: -1.0s } + .sk-rect4 { animation-delay: -0.9s } + .sk-rect5 { animation-delay: -0.8s } +} + +@keyframes sk-waveStretchDelay { + 0%, 40%, 100% { transform: scaleY(0.4) } + 20% { transform: scaleY(1.0) } +} diff --git a/client/components/main/spinner.tpl.jade b/client/components/main/spinner.tpl.jade new file mode 100644 index 00000000..9310a6e5 --- /dev/null +++ b/client/components/main/spinner.tpl.jade @@ -0,0 +1,6 @@ +.sk-spinner.sk-spinner-wave(class=currentBoard.colorClass) + .sk-rect1 + .sk-rect2 + .sk-rect3 + .sk-rect4 + .sk-rect5 diff --git a/client/components/main/templates.html b/client/components/main/templates.html new file mode 100644 index 00000000..e9be0f93 --- /dev/null +++ b/client/components/main/templates.html @@ -0,0 +1,18 @@ +<template name="notfound"> + {{ > message label='page-not-found'}} +</template> + +<template name='message'> + <div class="big-message quiet {{ color }}"> + <h1>{{_ label}}</h1> + {{#with pathFor route='Login'}} + <p>{{{_ 'page-maybe-private' this}}}</p> + {{/with}} + </div> +</template> + +<template name="editor"> + <textarea class="{{class}}" placeholder="{{_ 'comment-placeholder'}}" id="{{id}}" tabindex="1">{{> UI.contentBlock }}</textarea> +</template> + +<template name="viewer">{{#markdown}}{{#emoji}}{{#mentions}}{{> UI.contentBlock }}{{/mentions}}{{/emoji}}{{/markdown}}</template> diff --git a/client/components/modal/events.js b/client/components/modal/events.js new file mode 100644 index 00000000..2943f841 --- /dev/null +++ b/client/components/modal/events.js @@ -0,0 +1,14 @@ +Template.modal.events({ + 'click .window-overlay': function(event) { + // We only want to catch the event if the user click on the .window-overlay + // div itself, not a child (ie, not the overlay window) + if (event.target !== event.currentTarget) + return; + Utils.goBoardId(this.card.board()._id); + event.preventDefault(); + }, + 'click .js-close-window': function(event) { + Utils.goBoardId(this.card.board()._id); + event.preventDefault(); + } +}); diff --git a/client/components/modal/helpers.js b/client/components/modal/helpers.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/client/components/modal/helpers.js diff --git a/client/components/modal/modal.tpl.jade b/client/components/modal/modal.tpl.jade new file mode 100644 index 00000000..2f40ad75 --- /dev/null +++ b/client/components/modal/modal.tpl.jade @@ -0,0 +1,5 @@ +.window-overlay.show + .window + .window-wrapper.clearfix + a.icon-lg.fa.fa-times.dialog-close-button.js-close-window(title="{{_ 'modal-close-title'}}") + +UI.dynamic(template=template) diff --git a/client/components/sidebar/events.js b/client/components/sidebar/events.js new file mode 100644 index 00000000..1067421f --- /dev/null +++ b/client/components/sidebar/events.js @@ -0,0 +1,93 @@ +Template.filterSidebar.events({ + 'click .js-toggle-label-filter': function(event) { + Filter.labelIds.toogle(this._id); + Filter.resetExceptions(); + event.preventDefault(); + }, + 'click .js-toogle-member-filter': function(event) { + Filter.members.toogle(this._id); + Filter.resetExceptions(); + event.preventDefault(); + }, + 'click .js-clear-all': function(event) { + Filter.reset(); + event.preventDefault(); + } +}); + +var getMemberIndex = function(board, searchId) { + for (var i = 0; i < board.members.length; i++) { + if (board.members[i].userId === searchId) + return i; + } + throw new Meteor.Error('Member not found'); +}; + +Template.memberPopup.events({ + 'click .js-filter-member': function() { + Filter.members.toogle(this.userId); + Popup.close(); + }, + 'click .js-change-role': Popup.open('changePermissions'), + 'click .js-remove-member': Popup.afterConfirm('removeMember', function() { + var currentBoard = Boards.findOne(Session.get('currentBoard')); + var memberIndex = getMemberIndex(currentBoard, this.userId); + var setQuery = {}; + setQuery[['members', memberIndex, 'isActive'].join('.')] = false; + Boards.update(currentBoard._id, { $set: setQuery }); + Popup.close(); + }), + 'click .js-leave-member': function() { + // @TODO + Popup.close(); + } +}); + +Template.membersWidget.events({ + 'click .js-open-manage-board-members': Popup.open('addMember'), + 'click .member': Popup.open('member') +}); + +Template.labelsWidget.events({ + 'click .js-label': Popup.open('editLabel'), + 'click .js-add-label': Popup.open('createLabel') +}); + +// Template.addMemberPopup.events({ +// 'click .pop-over-member-list li:not(.disabled)': function(event, t) { +// var userId = this._id; +// var boardId = t.data.board._id; +// var currentMembersIds = _.pluck(t.data.board.members, 'userId'); +// if (currentMembersIds.indexOf(userId) === -1) { +// Boards.update(boardId, { +// $push: { +// members: { +// userId: userId, +// isAdmin: false, +// isActive: true +// } +// } +// }); +// } else { +// var memberIndex = getMemberIndex(t.data.board, userId); +// var setQuery = {}; +// setQuery[['members', memberIndex, 'isActive'].join('.')] = true; +// Boards.update(boardId, { $set: setQuery }); +// } +// Popup.close(); +// } +// }); + +// Template.changePermissionsPopup.events({ +// 'click .js-set-admin, click .js-set-normal': function(event) { +// var currentBoard = Boards.findOne(Session.get('currentBoard')); +// var memberIndex = getMemberIndex(currentBoard, this.user._id); +// var isAdmin = $(event.currentTarget).hasClass('js-set-admin'); +// var setQuery = {}; +// setQuery[['members', memberIndex, 'isAdmin'].join('.')] = isAdmin; +// Boards.update(currentBoard._id, { +// $set: setQuery +// }); +// Popup.back(1); +// } +// }); diff --git a/client/components/sidebar/helpers.js b/client/components/sidebar/helpers.js new file mode 100644 index 00000000..a76dad7f --- /dev/null +++ b/client/components/sidebar/helpers.js @@ -0,0 +1,51 @@ +var widgetTitles = { + filter: 'filter-cards', + background: 'change-background' +}; + +Template.boardSidebar.helpers({ + currentWidget: function() { + return Session.get('currentWidget') + 'Sidebar'; + }, + currentWidgetTitle: function() { + return TAPi18n.__(widgetTitles[Session.get('currentWidget')]); + } +}); + +// Template.addMemberPopup.helpers({ +// isBoardMember: function() { +// var user = Users.findOne(this._id); +// return user && user.isBoardMember(); +// } +// }); + +Template.memberPopup.helpers({ + user: function() { + return Users.findOne(this.userId); + }, + memberType: function() { + var type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal'; + return TAPi18n.__(type).toLowerCase(); + } +}); + +// Template.removeMemberPopup.helpers({ +// user: function() { +// return Users.findOne(this.userId) +// }, +// board: function() { +// return currentBoard(); +// } +// }); + +// Template.changePermissionsPopup.helpers({ +// isAdmin: function() { +// return this.user.isBoardAdmin(); +// }, +// isLastAdmin: function() { +// if (! this.user.isBoardAdmin()) +// return false; +// var nbAdmins = _.where(currentBoard().members, { isAdmin: true }).length; +// return nbAdmins === 1; +// } +// }); diff --git a/client/components/sidebar/infiniteScrolling.js b/client/components/sidebar/infiniteScrolling.js new file mode 100644 index 00000000..df3b8901 --- /dev/null +++ b/client/components/sidebar/infiniteScrolling.js @@ -0,0 +1,37 @@ +var peakAnticipation = 200; + +Mixins.InfiniteScrolling = BlazeComponent.extendComponent({ + onCreated: function() { + this._nextPeak = Infinity; + }, + + setNextPeak: function(v) { + this._nextPeak = v; + }, + + getNextPeak: function() { + return this._nextPeak; + }, + + resetNextPeak: function() { + this._nextPeak = Infinity; + }, + + // To be overwritten by consumers of this mixin + reachNextPeak: function() { + + }, + + events: function() { + return [{ + scroll: function(evt) { + var domElement = evt.currentTarget; + var altitude = domElement.scrollTop + domElement.offsetHeight; + altitude += peakAnticipation; + if (altitude >= this.callFirstWith(null, 'getNextPeak')) { + this.callFirstWith(null, 'reachNextPeak'); + } + } + }]; + } +}); diff --git a/client/components/sidebar/rendered.js b/client/components/sidebar/rendered.js new file mode 100644 index 00000000..2b58bf33 --- /dev/null +++ b/client/components/sidebar/rendered.js @@ -0,0 +1,21 @@ +Template.membersWidget.rendered = function() { + if (! Meteor.user().isBoardMember()) + return; + + _.each(['.js-member', '.js-label'], function(className) { + Utils.liveEvent('mouseover', function($this) { + $this.find(className).draggable({ + appendTo: 'body', + helper: 'clone', + revert: 'invalid', + revertDuration: 150, + snap: false, + snapMode: 'both', + start: function() { + Popup.close(); + } + }); + }); + }); +}; + diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js new file mode 100644 index 00000000..3f0142d4 --- /dev/null +++ b/client/components/sidebar/sidebar.js @@ -0,0 +1,55 @@ +BlazeComponent.extendComponent({ + template: function() { + return 'boardSidebar'; + }, + + mixins: function() { + return [Mixins.InfiniteScrolling]; + }, + + onCreated: function() { + this._isOpen = new ReactiveVar(true); + }, + + isOpen: function() { + return this._isOpen.get(); + }, + + open: function() { + if (! this._isOpen.get()) { + this._isOpen.set(true); + } + }, + + hide: function() { + if (this._isOpen.get()) { + this._isOpen.set(false); + } + }, + + toogle: function() { + this._isOpen.set(! this._isOpen.get()); + }, + + calculateNextPeak: function() { + var altitude = this.find('.js-board-sidebar-content').scrollHeight; + this.callFirstWith(this, 'setNextPeak', altitude); + }, + + reachNextPeak: function() { + var activitiesComponent = this.componentChildren('activities')[0]; + activitiesComponent.loadNextPage(); + }, + + isTongueHidden: function() { + return this.isOpen() && Filter.isActive(); + }, + + events: function() { + // XXX Hacky, we need some kind of `super` + var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events(); + return mixinEvents.concat([{ + 'click .js-toogle-sidebar': this.toogle + }]); + } +}).register('boardSidebar'); diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl new file mode 100644 index 00000000..4b741dc7 --- /dev/null +++ b/client/components/sidebar/sidebar.styl @@ -0,0 +1,154 @@ +@import 'nib' + +.sidebar + .sidebar-content + padding: 10px 20px + background: white + box-shadow: -10px 0px 5px -10px darken(white, 30%) + z-index: 10 + position: absolute + top: 0 + bottom: 0 + right: 0 + left: 0 + overflow-x: hidden + overflow-y: auto + + h3 + color: darken(white, 50%) + + hr + margin: 8px 0 + +.board-sidebar + width: 248px + position: absolute + top: 0 + right: -@width + bottom: 0 + transition: top .1s, right .1s, width .1s + + &.is-open + right: 0 + +.board-widget-nav + border-radius: 3px + background: #dcdcdc + overflow: hidden + padding: 0 + position: relative + + .toggle-widget-nav + border-radius: 3px + color: #8c8c8c + margin: 0 + padding: 7px 10px + position: relative + cursor: pointer + + .toggle-menu-icon + position: absolute + top: 8px + right: 8px + + &:hover + background: #ccc + color: #4d4d4d + + .nav-list + display: block + opacity: 1 + max-height: 400px + + hr + margin: 2px 0 + color: #ccc + background: #ccc + + .nav-list-item + display: block + font-weight: 700 + line-height: 30px + overflow: hidden + padding: 0 8px 0 36px + position: relative + text-decoration: none + + .icon-type + left: 10px + position: absolute + top: 6px + + &:hover + background: #ccc + + .icon-type + color: #686868 + + .nav-list-sub-item + font-weight: 400 + color: #666 + + &:hover + color: #4d4d4d + + &.collapsed + + .nav-list + max-height: 0 + opacity: 0 + + hr + margin: 0 + + .toggle-widget-nav + color: #4d4d4d + + +.board-widget-title + display: block + min-height: 20px + margin-bottom: 6px + +.board-widget-content + position: relative + z-index: 1 + +.board-widget h4 + margin: 5px 0 + +.board-widget-activity + margin-right: -4px + +.sidebar-tongue + display: block + width: 30px + height: @width + left: -@width + position: absolute + top: 12px + z-index: 15 + background: white + border-radius: left 3px + box-shadow: -4px 0px 7px -4px darken(white, 30%) + color: darken(white, 50%) + transition: left .1s + + i.fa + margin: 9px + transition: transform 0.5s + + .board-sidebar.is-open & + left: -@width + 2px + + // XXX Bug: we should add a padding left + &:hover + left: -@width + 5px + + i.fa + transform: rotate(180deg) + + &.is-hidden, + .board-sidebar.is-open &.is-hidden + z-index: 0 + left: 5px diff --git a/client/components/sidebar/templates.html.old b/client/components/sidebar/templates.html.old new file mode 100644 index 00000000..d8b063f0 --- /dev/null +++ b/client/components/sidebar/templates.html.old @@ -0,0 +1,307 @@ +<template name="boardWidgets"> + <a href="#" class="sidebar-show-btn dark-hover js-show-sidebar"> + <span class="icon-sm fa fa-chevron-left"></span> + <span class="text">{{_ 'show-sidebar'}}</span> + </a> + <div class="board-widgets {{#if session 'sidebarIsOpen'}}show{{else}}hide{{/if}}"> + <div> + <a href="#" class="sidebar-hide-btn dark-hover js-hide-sidebar" title="{{_ 'close-sidebar-title'}}"> + <span class="icon-sm fa fa-chevron-right"></span> + </a> + {{#unless isTrue currentWidget "homeWidget"}} + <div class="board-widgets-title clearfix"> + <a href="#" class="board-sidebar-back-btn js-pop-widget-view"> + <span class="left-arrow"></span>{{_ 'back'}} + </a> + <h3 class="text">{{currentWidgetTitle}}</h3> + <hr> + </div> + {{/unless}} + <div class="board-widgets-content-wrapper"> + <div class="board-widgets-content default fancy-scrollbar short{{#unless session 'menuWidgetIsOpen'}} short{{/unless}}"> + {{> UI.dynamic template=currentWidget data=this }} + </div> + </div> + </div> + </div> +</template> + +<template name="homeWidget"> +{{ > menuWidget }} +{{ > membersWidget }} +{{ > activityWidget }} +</template> + +<template name="menuWidget"> + <div class="board-widget board-widget-nav clearfix{{#unless session 'menuWidgetIsOpen'}} collapsed{{/unless}}"> + <h3 class="dark-hover toggle-widget-nav js-toggle-widget-nav">{{_ 'menu'}} + <span class="icon-sm fa fa-chevron-circle-down toggle-menu-icon"></span> + </h3> + <ul class="nav-list"> + <hr style="margin-top: 0;"> + <li> + <a href="#" class="nav-list-item js-open-archive"> + <span class="icon-sm fa fa-archive icon-type"></span> + {{_ 'archived-items'}} + </a> + </li> + <li> + <a href="#" class="nav-list-item js-open-card-filter"> + <span class="icon-sm fa fa-filter icon-type"></span> + {{_ 'filter-cards'}} + </a> + </li> + {{#if currentUser.isBoardAdmin}} + <hr> + <li> + <a class="nav-list-item nav-list-sub-item board-settings-background js-change-background"> + <span class="board-settings-background-preview" style="background-color:{{board.background.color}}"></span> + {{_ 'change-background'}}… + </a> + </li> + {{#unless isSandstorm }} + <li> + <a class="nav-list-item nav-list-sub-item js-close-board" href="#">{{_ 'close-board'}}</a> + </li> + {{/unless}} + {{/if}} + {{! + XXX Language should be handled by sandstorm, but for now display a language selection link in the board menu. + This link is normally present in the header bar that is not displayed on sandstorm. + }} + {{#if isSandstorm}} + <hr> + <li> + <a class="nav-list-item nav-list-sub-item js-language">{{_ 'language'}}</a> + </li> + {{/if}} + </ul> + </div> +</template> + +<template name="membersWidget"> + <hr> + <div class="board-widget board-widget-members clearfix"> + <div class="board-widget-title"> + <h3>{{_ 'members'}}</h3> + </div> + <div class="board-widget-content"> + <div class="board-widget-members js-list-board-members clearfix js-list-draggable-board-members"> + {{# each board.members }} + {{> userAvatar userId=this.userId draggable=true size="small" showBadges=true}} + {{/ each }} + </div> + {{# unless isSandstrom }} + {{# if currentUser.isBoardAdmin }} + <a href="#" class="button-link js-open-manage-board-members"> + <span class="icon-sm fa fa-user"></span> {{_ 'add-members'}} + </a> + {{/ if }} + {{/ unless }} + </div> + </div> +</template> + +<template name="activityWidget"> + {{# if board.activities.count }} + <hr> + <div class="board-widget board-widget-activity bottom clearfix"> + <div class="board-widget-title"> + <h3>{{_ 'activity'}}</h3> + </div> + <div class="board-widget-content"> + <div class="activity-gradient-t"></div> + <div class="activity-gradient-b"></div> + <div class="board-actions-list fancy-scrollbar"> + {{ > activities }} + </div> + </div> + </div> + {{/if}} +</template> + +<template name="memberPopup"> + <div class="board-member-menu"> + <div class="mini-profile-info"> + {{> userAvatar user=user}} + <div class="info"> + <h3 class="bottom" style="margin-right: 40px;"> + <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a> + </h3> + <p class="quiet bottom">@{{ user.username }}</p> + </div> + </div> + {{# if currentUser.isBoardMember }} + <ul class="pop-over-list"> + {{# if currentUser.isBoardAdmin }} + <li> + <a class="js-change-role" href="#"> + {{_ 'change-permissions'}} <span class="quiet" style="font-weight: normal;">({{ memberType }})</span> + </a> + </li> + {{/ if }} + + <li> + {{# if currentUser.isBoardAdmin }} + <a class="js-remove-member">{{_ 'remove-from-board'}}</a> + {{ else }} + <a class="js-leave-member">{{_ 'leave-board'}}</a> + {{/ if }} + </li> + </ul> + {{/ if }} + </div> +</template> + +<template name="filterWidget"> + <ul class="pop-over-label-list checkable"> + {{#each board.labels}} + <li class="item matches-filter"> + <a class="name js-toggle-label-filter"> + <span class="card-label card-label-{{color}}"></span> + <span class="full-name"> + {{#if name}} + {{name}} + {{else}} + <span class="quiet">{{_ "label-default" color}}</span> + {{/if}} + </span> + {{#if Filter.labelIds.isSelected _id}} + <span class="icon-sm fa fa-check"></span> + {{/if}} + </a> + </li> + {{/each}} + </ul> + <hr> + <ul class="pop-over-member-list checkable"> + {{#each board.members}} + {{#with getUser userId}} + <li class="item js-member-item {{#if Filter.members.isSelected _id}}active{{/if}}"> + <a href="#" class="name js-toogle-member-filter"> + {{> userAvatar user=this size="small" }} + <span class="full-name"> + {{ profile.name }} + (<span class="username">{{ username }}</span>) + </span> + {{#if Filter.members.isSelected _id}} + <span class="icon-sm fa fa-check checked-icon"></span> + {{/if}} + </a> + </li> + {{/with}} + {{/each}} + </ul> + <hr> + <ul class="pop-over-list inset normal-weight"> + <li> + <a class="js-clear-all {{#unless Filter.isActive}}disabled{{/unless}}" style="padding-left: 40px;"> + {{_ 'filter-clear'}} + </a> + </li> + </ul> +</template> + +<template name="backgroundWidget"> + <div class="board-widgets-content-wrapper fancy-scrollbar"> + <div class="board-widgets-content"> + <div class="board-backgrounds-list clearfix"> + {{#each backgroundColors}} + <div class="board-background-select js-select-background"> + <span class="background-box " style="background-color: {{this}}; "></span> + </div> + {{/each}} + </div> + {{!-- + <h2 class="clear">Photos</h2> + <div class="board-backgrounds-list relative clearfix js-gold-photos-list disabled"> + <div class="board-background-select js-select-background"> + <span class="background-box " style="background-image: url("{{url}}");"> + <a class="background-option js-background-attribution" href={{href}} target="_blank" title={{title}}> + <img src="https://d78fikflryjgj.cloudfront.net/images/d906fe5c1274c56c5571d49705547587/cc.png" style="height: 14px; width: 14px; vertical-align: text-top;" title="http://creativecommons.org/licenses/by/2.0/deed.en"> + <span class="text" style="margin-left: 2px;">{{author}}</span> + </a> + </span> + </div> + </div> + --}} + </div> + </div> +</template> + +<template name="closeBoardPopup"> + <p>{{_ 'close-board-pop'}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'close'}}"> +</template> + +<template name="removeMemberPopup"> + <p>{{_ 'remove-member-pop' + name=user.profile.name + username=user.username + boardTitle=board.title}}</p> + <input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}"> +</template> + +<template name="addMemberPopup"> + <div class="search-with-spinner"> + {{> esInput index="users" }} + </div> + + <div class="manage-member-section hide js-search-results" style="display: block;"> + <ul class="pop-over-member-list options js-list"> + {{# esEach index="users"}} + <li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}"> + <a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})"> + {{> userAvatar user=this size="small" }} + <span class="full-name"> + {{ profile.name }} (<span class="username">{{ username }}</span>) + </span> + {{# if isBoardMember }} + <div class="extra-text quiet">({{_ 'joined'}})</div> + {{/if}} + <span class="icon-sm fa fa-chevron-right light option js-open-option"></span> + </a> + </li> + {{/esEach }} + </ul> + </div> + + {{# ifEsIsSearching index='users' }} + <div class="tac"> + <span class="tabbed-pane-main-col-loading-spinner spinner"></span> + </div> + {{ /ifEsIsSearching }} + + {{# ifEsHasNoResults index="users" }} + <div class="manage-member-section js-no-results"> + <p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p> + </div> + {{ /ifEsHasNoResults }} + + <div class="manage-member-section js-helper"> + <p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p> + </div> +</template> + +<template name="changePermissionsPopup"> + <ul class="pop-over-list"> + <li> + <a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}"> + {{_ 'admin'}} + {{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}} + <span class="sub-name">{{_ 'admin-desc'}}</span> + </a> + </li> + <li> + <a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}"> + {{_ 'normal'}} + {{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}} + <span class="sub-name">{{_ 'normal-desc'}}</span> + </a> + </li> + </ul> + {{#if isLastAdmin}} + <hr> + <p class="quiet bottom">{{_ 'last-admin-desc'}}</p> + {{/if}} +</template> diff --git a/client/components/sidebar/templates.jade b/client/components/sidebar/templates.jade new file mode 100644 index 00000000..23a1a87e --- /dev/null +++ b/client/components/sidebar/templates.jade @@ -0,0 +1,103 @@ +template(name="boardSidebar") + .board-sidebar.sidebar(class="{{#if isOpen}}is-open{{/if}}") + a.sidebar-tongue.js-toogle-sidebar( + class="{{#if isTongueHidden}}is-hidden{{/if}}") + i.fa.fa-chevron-left + .sidebar-content.js-board-sidebar-content + //- XXX https://github.com/peerlibrary/meteor-blaze-components/issues/30 + if Filter.isActive + +filterSidebar + else + +homeSidebar + +template(name='homeSidebar') + +membersWidget + hr.clear + +labelsWidget + hr.clear + h3 + i.fa.fa-comments-o + | {{_ 'activities'}} + +activities(mode="board") + +template(name="filterSidebar") + ul.pop-over-label-list.checkable + each currentBoard.labels + li.item.matches-filter + a.name.js-toggle-label-filter + span.card-label(class="card-label-{{color}}") + span.full-name + if name + = name + else + span.quiet {{_ "label-default" color}} + if Filter.labelIds.isSelected _id}} + span.icon-sm.fa.fa-check + hr + ul.pop-over-member-list.checkable + each currentBoard.members + if isActive + with getUser userId + li.item.js-member-item( + class="{{#if Filter.members.isSelected _id}}active{{/if}}") + a.name.js-toogle-member-filter + +userAvatar(user=this size="small") + span.full-name + = profile.name + | (<span class="username">{{ username }}</span>) + if Filter.members.isSelected _id + span.icon-sm.fa.fa-check + hr + a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}") + | {{_ 'filter-clear'}} + +template(name="membersWidget") + .board-widget.board-widget-members + h3 + i.fa.fa-user + | {{_ 'members'}} + .board-widget-content + each currentBoard.members + +userAvatar( + userId=this.userId + draggable=true + size="small" + showBadges=true) + unless isSandstorm + if currentUser.isBoardAdmin + a.js-open-manage-board-members + +template(name="labelsWidget") + .board-widget.board-widget-labels + h3 + i.fa.fa-tags + | {{_ 'labels'}} + .board-widget-content + each currentBoard.labels + a.card-label(class="card-label-{{color}}").js-label + span.card-label-name= name + a.card-label.js-add-label + i.fa.fa-plus + +template(name="memberPopup") + .board-member-menu: .mini-profile-info + +userAvatar(user=user) + .info + h3.bottom + a.js-profile(href="{{pathFor route='Profile' username=user.username}}") + = user.profile.name + p.quiet.bottom @#{user.username} + if currentUser.isBoardMember + ul.pop-over-list + li + a.js-filter-member Filter cards + if currentUser.isBoardAdmin + li + a.js-change-role + | {{_ 'change-permissions'}} + span.quiet (#{memberType}) + li + if currentUser.isBoardAdmin + a.js-remove-member {{_ 'remove-from-board'}} + else + a.js-leave-member {{_ 'leave-board'}} diff --git a/client/components/users/avatar.jade b/client/components/users/avatar.jade new file mode 100644 index 00000000..70ef69e0 --- /dev/null +++ b/client/components/users/avatar.jade @@ -0,0 +1,7 @@ +template(name="userAvatar") + .member(class="{{class}} {{# if draggable }}js-member{{else}}js-member-on-card-menu{{/if}}" + title="{{userData.profile.name}} ({{userData.username}})") + +avatar(user=userData size=size) + if showBadges + span.member-status(class="{{# if userData.profile.status}}active{{/if}}") + span.member-type(class=memberType) diff --git a/client/components/users/events.js b/client/components/users/events.js new file mode 100644 index 00000000..14df9717 --- /dev/null +++ b/client/components/users/events.js @@ -0,0 +1,59 @@ +// XXX This should be handled by default (and in a better way) by useraccounts. +// See https://github.com/meteor-useraccounts/core/issues/384 +Template.atForm.onRendered(function() { + this.find('input').focus(); +}); + +Template.memberMenuPopup.events({ + 'click .js-language': Popup.open('setLanguage'), + 'click .js-logout': function(evt) { + evt.preventDefault(); + + Meteor.logout(function() { + Router.go('Home'); + }); + } +}); + +Template.setLanguagePopup.events({ + 'click .js-set-language': function(evt) { + Users.update(Meteor.userId(), { + $set: { + 'profile.language': this.tag + } + }); + evt.preventDefault(); + } +}); + +Template.profileEditForm.events({ + 'click .js-edit-profile': function() { + Session.set('ProfileEditForm', true); + }, + 'click .js-cancel-edit-profile': function() { + Session.set('ProfileEditForm', false); + }, + 'submit #ProfileEditForm': function(evt, t) { + var name = t.find('#name').value; + var bio = t.find('#bio').value; + + // trim and update + if ($.trim(name)) { + Users.update(this.profile()._id, { + $set: { + 'profile.name': name, + 'profile.bio': bio + } + }, function() { + + // update complete close profileEditForm + Session.set('ProfileEditForm', false); + }); + } + evt.preventDefault(); + } +}); + +Template.memberName.events({ + 'click .js-show-mem-menu': Popup.open('user') +}); diff --git a/client/components/users/form.styl b/client/components/users/form.styl new file mode 100644 index 00000000..845c810d --- /dev/null +++ b/client/components/users/form.styl @@ -0,0 +1,50 @@ +.at-form-landing-logo + width: 275px + margin: auto + margin-top: 50px + margin-top: 17vh + + img + width: 275px + + +.at-form + margin: auto + width: 275px + padding: 25px + margin-top: 20px + padding-bottom: 10px + background: #fff + border-radius: 3px + border: 1px solid #dbdbdb + border-bottom-color: #c2c2c2 + box-shadow: 0 1px 6px rgba(0, 0, 0, .3) + + .at-link + color: darken(#27AE60, 40%) + + label + margin-bottom: 3px + + input + width: 100% + + .at-title + background: #F7F7F7 + margin: -25px + padding: 15px 25px 5px + margin-bottom: 20px + border-bottom: 1px solid #dcdcdc + color: darken(white, 70%) + font-weight: bold + + .at-signup-link, + .at-signin-link, + .at-forgotPwd + font-size: 0.9em + margin-top: 15px + color: darken(white, 70%) + + .at-signUp, + .at-signIn + font-weight: bold diff --git a/client/components/users/headerButtons.jade b/client/components/users/headerButtons.jade new file mode 100644 index 00000000..74c24ad5 --- /dev/null +++ b/client/components/users/headerButtons.jade @@ -0,0 +1,27 @@ +template(name="headerUserBar") + #header-user-bar + if currentUser + a.js-open-header-member-menu + if currentUser.profile.name + = currentUser.profile.name + else + = currentUser.username + i.fa.fa-chevron-down + else + a(href="{{pathFor route='signUp'}}") Sign in + span.separator - + a(href="{{pathFor route='signIn'}}") Log in + +template(name="memberHeader") + a.header-member.js-open-header-member-menu + span= currentUser.profile.name + +userAvatar(user=currentUser size="small") + +template(name="memberMenuPopup") + ul.pop-over-list + li: a(href="{{pathFor route='Profile' username=currentUser.username}}") {{_ 'profile'}} + li: a.js-language {{_ 'language'}} + li: a(href = "{{pathFor route='Settings'}}") {{_ 'settings'}} + hr + ul.pop-over-list + li: a.js-logout {{_ 'log-out'}} diff --git a/client/components/users/headerButtons.js b/client/components/users/headerButtons.js new file mode 100644 index 00000000..70594fb5 --- /dev/null +++ b/client/components/users/headerButtons.js @@ -0,0 +1,5 @@ +Template.headerUserBar.events({ + 'click .js-sign-in': Popup.open('signup'), + 'click .js-log-in': Popup.open('login'), + 'click .js-open-header-member-menu': Popup.open('memberMenu') +}); diff --git a/client/components/users/helpers.js b/client/components/users/helpers.js new file mode 100644 index 00000000..33867298 --- /dev/null +++ b/client/components/users/helpers.js @@ -0,0 +1,27 @@ +Template.userAvatar.helpers({ + userData: function() { + if (! this.user) { + this.user = Users.findOne(this.userId); + } + return this.user; + }, + memberType: function() { + var userId = this.userId || this.user._id; + var user = Users.findOne(userId); + return user && user.isBoardAdmin() ? 'admin' : 'normal'; + } +}); + +Template.setLanguagePopup.helpers({ + languages: function() { + return _.map(TAPi18n.getLanguages(), function(lang, tag) { + return { + tag: tag, + name: lang.name + }; + }); + }, + isCurrentLanguage: function() { + return this.tag === TAPi18n.getLanguage(); + } +}); diff --git a/client/components/users/member.styl b/client/components/users/member.styl new file mode 100644 index 00000000..3dfdaa37 --- /dev/null +++ b/client/components/users/member.styl @@ -0,0 +1,107 @@ +@import 'nib' + +avatar-radius = 50% + +.member + border-radius: 3px + display: block + float: left + height: 30px + width: @height + margin: 0 4px 4px 0 + position: relative + cursor: pointer + user-select: none + z-index: 1 + text-decoration: none + border-radius: avatar-radius + + .avatar + height: 100% + width: @height + display: flex + align-items: center + justify-content: center + overflow: hidden + border-radius: avatar-radius + + .avatar-initials + font-weight: bold + max-width: 100% + max-height: 100% + font-size: 14px + line-height: 200% + background-color: #dbdbdb + color: #444444 + + .avatar-image + max-width: 100% + max-height: 100% + + .member-status + background-color: #b3b3b3 + border: 1px solid #fff + border-radius: 50% + height: 8px + width: @height + position: absolute + right: 0px + bottom: 0px + border: 1px solid white + + &.active + background: #64c464 + border-color: #daf1da + + &.idle + background: #e4e467 + border-color: #f7f7d4 + + &.disconnected + background: #bdbdbd + border-color: #ededed + + &.extra-small + .avatar-initials + font-size: 9px + width: 18px + height: 18px + line-height: 18px + + .avatar-image + width: 18px + height: 18px + + &.small + width: 30px + height: 30px + + .avatar-initials + font-size: 12px + line-height: 30px + + &.large + height: 85px + line-height: 85px + width: 85px + + .avatar + width: 85px + height: 85px + + .avatar-initials + font-size: 16px + font-weight: 700 + line-height: 85px + width: 85px + +.atMention + background: #dbdbdb + border-radius: 3px + padding: 1px 4px + margin: -1px 0 + display: inline-block + + &.me + background: #cfdfe8 + diff --git a/client/components/users/router.js b/client/components/users/router.js new file mode 100644 index 00000000..d59e174d --- /dev/null +++ b/client/components/users/router.js @@ -0,0 +1,29 @@ + +_.each(['signIn', 'signUp', 'resetPwd', + 'forgotPwd', 'enrollAccount', 'changePwd'], function(routeName) { + AccountsTemplates.configureRoute(routeName, { + layoutTemplate: 'userFormsLayout' + }); +}); + +Router.route('/profile/:username', { + name: 'Profile', + template: 'profile', + waitOn: function() { + return Meteor.subscribe('profile', this.params.username); + }, + data: function() { + var params = this.params; + return { + profile: function() { + return Users.findOne({ username: params.username }); + } + }; + } +}); + +Router.route('/settings', { + name: 'Settings', + template: 'settings', + layoutTemplate: 'AuthLayout' +}); diff --git a/client/components/users/templates.html b/client/components/users/templates.html new file mode 100644 index 00000000..5783eebf --- /dev/null +++ b/client/components/users/templates.html @@ -0,0 +1,118 @@ +<template name="setLanguagePopup"> +<ul class="pop-over-list"> + {{#each languages}} + <li class="{{# if isCurrentLanguage}}active{{/if}}"> + <a class="js-set-language"> + {{name}} + {{# if isCurrentLanguage}} + <span class="icon-sm fa fa-check"></span> + {{/if}} + </a> + </li> + {{/each}} +</ul> +</template> + +<template name='profile'> + {{ # if profile }} + <div class="tabbed-pane-header"> + <div class="tabbed-pane-header-wrapper clearfix"> + <a class="tabbed-pane-header-image profile-image ed js-change-avatar-profile" href="#"> + {{> userAvatar user=profile size="large"}} + </a> + <div class="tabbed-pane-header-details"> + <div class="js-current-details"> + <div class="tabbed-pane-header-details-name"> + <h1 class="inline"> {{ profile.profile.name }} </h1> + <p class="window-title-extra quiet"> @{{ profile.username }} </p> + </div> + <div class="tabbed-pane-header-details-content"> + <p>{{ profile.profile.bio }}</p> + </div> + <div class="tabbed-pane-header-details-content"></div> + </div> + {{ > profileEditForm }} + </div> + </div> + </div> + {{ else }} + {{ > message label='user-profile-not-found' }} + {{ /if }} +</template> + +<template name="settings"> + {{ > profile profile=currentUser }} + <div class="tabbed-pane-main-col clearfix"> + <div class="tabbed-pane-main-col-loading hide js-loading-page"> + <span class="tabbed-pane-main-col-loading-spinner spinner"></span> + </div> + <div class="tabbed-pane-main-col-wrapper js-content"> + <div class="window-module clearfix"> + <div class="window-module-title"> + <h3>{{_ "account-details"}}</h3> + </div> + <a class="big-link js-change-name-and-bio" href="#"> + <span class="text">{{_ 'change-name-initials-bio'}}</span> + </a> + <a class="big-link js-change-avatar" href="#"> + <span class="text">{{_ 'change-avatar'}}</span> + </a> + <a class="big-link js-change-password" href="#"> + <span class="text">{{_ 'change-password'}}</span> + </a> + <a class="big-link js-change-email" href="#"> + <span class="text">{{_ 'change-email'}}</span> + </a> + </div> + </div> + </div> +</template> + +<template name="profileEditForm"> + {{#if $eq currentUser.username profile.username }} + {{# if session 'ProfileEditForm' }} + <form id="ProfileEditForm" class="js-profile-form"> + <p class="error js-profile-form-error hide"></p> + <label>{{_ "username"}}</label> + <input type="text" id="username" value="{{ profile.username }}" disabled> + <label>{{_ "fullname"}}</label> + <input type="text" id="name" value="{{ profile.profile.name }}"> + <label> + {{_ "bio"}} <span class="quiet">({{_ 'optional'}})</span> + </label> + <textarea id="bio">{{ profile.profile.bio }}</textarea> + <input type="submit" class="primary wide js-submit-profile" value="{{_ 'save'}}"> + <input type="button" class="js-cancel-edit-profile" value="{{_ 'cancel'}}"> + </form> + {{ else }} + <a class="button-link tabbed-pane-header-details-edit js-edit-profile" href="#"> + <span class="icon-sm fa fa-pencil"></span> + {{_ "edit-profile"}} + </a> + {{ /if }} + {{ /if }} +</template> + +<template name="userPopup"> + <div class="board-member-menu"> + <div class="mini-profile-info"> + {{> userAvatar user=user}} + <div class="info"> + <h3 class="bottom" style="margin-right: 40px;"> + <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a> + </h3> + <p class="quiet bottom">@{{ user.username }}</p> + </div> + </div> + </div> +</template> + + +<template name="memberName"> + <a class="inline-object js-show-mem-menu" href="{{ pathFor route='Profile' username=user.username }}"> + {{ user.profile.name }} + {{# if username }} + ({{ user.username }}) + {{ /if }} + </a> +</template> |