├── .meteor ├── .gitignore ├── cordova-plugins ├── release ├── platforms ├── .finished-upgraders ├── .id ├── packages └── versions ├── client ├── views │ ├── modal │ │ ├── helpers.js │ │ ├── templates.html │ │ └── events.js │ ├── activities │ │ ├── helpers.js │ │ ├── events.js │ │ └── templates.html │ ├── lists │ │ ├── helpers.js │ │ ├── rendered.js │ │ ├── templates.html │ │ └── events.js │ ├── main │ │ ├── events.js │ │ ├── rendered.js │ │ ├── routers.js │ │ ├── helpers.js │ │ └── templates.html │ ├── widgets │ │ ├── rendered.js │ │ ├── helpers.js │ │ └── events.js │ ├── users │ │ ├── helpers.js │ │ ├── routers.js │ │ ├── events.js │ │ └── member.styl │ ├── boards │ │ ├── rendered.js │ │ ├── helpers.js │ │ ├── routers.js │ │ ├── events.js │ │ ├── header.styl │ │ ├── templates.html │ │ └── body.styl │ └── cards │ │ ├── routers.js │ │ ├── helpers.js │ │ ├── rendered.js │ │ └── labels.styl ├── lib │ ├── i18n.js │ ├── filter.js │ ├── utils.js │ └── popup.js └── styles │ ├── datepicker.import.styl │ ├── print.styl │ ├── metrello.css │ ├── aging.import.styl │ ├── icons.styl │ └── sticker.import.styl ├── package.json ├── public ├── favicon.png ├── loading.gif ├── logos │ ├── logo.png │ ├── blue_logo.png │ └── white_logo.png ├── developers │ ├── maxime.png │ └── yasaricli.png └── fonts │ ├── trellicons-regular.ttf │ └── trellicons-regular.woff ├── .gitignore ├── server ├── publications │ ├── cards.js │ ├── users.js │ └── boards.js └── migrations.js ├── .travis.yml ├── lib ├── prototypes.js └── utils.js ├── Dockerfile ├── History.md ├── LICENSE ├── README.md ├── collections ├── activities.js ├── users.js ├── attachments.js ├── lists.js └── boards.js ├── sandstorm.js ├── sandstorm-pkgdef.capnp ├── Contributing.md └── i18n ├── ja.i18n.json ├── tr.i18n.json ├── fr.i18n.json └── en.i18n.json /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/cordova-plugins: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/views/modal/helpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.2.1 2 | -------------------------------------------------------------------------------- /client/views/activities/helpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { } 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/loading.gif -------------------------------------------------------------------------------- /public/logos/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/logos/logo.png -------------------------------------------------------------------------------- /client/views/lists/helpers.js: -------------------------------------------------------------------------------- 1 | Template.addlistForm.helpers({}); 2 | Template.lists.helpers({}); 3 | -------------------------------------------------------------------------------- /public/logos/blue_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/logos/blue_logo.png -------------------------------------------------------------------------------- /public/developers/maxime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/developers/maxime.png -------------------------------------------------------------------------------- /public/logos/white_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/logos/white_logo.png -------------------------------------------------------------------------------- /public/developers/yasaricli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/developers/yasaricli.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # text editors 2 | *~ 3 | *.swp 4 | 5 | private/conf.js 6 | private/production.js 7 | .meteor-spk 8 | .tx/ 9 | -------------------------------------------------------------------------------- /public/fonts/trellicons-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/fonts/trellicons-regular.ttf -------------------------------------------------------------------------------- /public/fonts/trellicons-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ncarlier/libreboard/HEAD/public/fonts/trellicons-regular.woff -------------------------------------------------------------------------------- /server/publications/cards.js: -------------------------------------------------------------------------------- 1 | Meteor.publish('card', function(cardId) { 2 | check(cardId, String); 3 | return Cards.find({ _id: cardId }); 4 | }); 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - "curl -L http://git.io/ejPSng | /bin/sh" 6 | services: 7 | - mongodb 8 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /lib/prototypes.js: -------------------------------------------------------------------------------- 1 | String.prototype.format = function() { 2 | var formatted = this; 3 | for (var i = 0; i < arguments.length; i++) { 4 | var regexp = new RegExp('\\{'+i+'\\}', 'gi'); 5 | formatted = formatted.replace(regexp, arguments[i]); 6 | } 7 | return formatted; 8 | }; 9 | -------------------------------------------------------------------------------- /client/views/main/events.js: -------------------------------------------------------------------------------- 1 | Template.editor.events({ 2 | // Pressing Ctrl+Enter should submit the form. 3 | 'keydown textarea': function(event, t) { 4 | if (event.keyCode == 13 && (event.metaKey || event.ctrlKey)) { 5 | $(event.currentTarget).parents('form:first').submit(); 6 | } 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | dvyihgykyzec6y1dpg 8 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | allowIsBoardAdmin = function(userId, board) { 2 | return _.contains(_.pluck(_.where(board.members, {isAdmin: true}), 'userId'), userId); 3 | }; 4 | 5 | allowIsBoardMember = function(userId, board) { 6 | return _.contains(_.pluck(board.members, 'userId'), userId); 7 | }; 8 | 9 | isServer = function(callback) { 10 | return Meteor.isServer && callback(); 11 | }; 12 | -------------------------------------------------------------------------------- /client/views/modal/templates.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grigio/meteor:1.0.2.1 2 | 3 | # Add the source of your Meteor app and build 4 | ADD . /app 5 | RUN /meteor-build.sh 6 | 7 | # Run the generated files 8 | CMD /meteor-run.sh 9 | 10 | ## Build the image 11 | # sudo docker build -t grigio/metrello . 12 | 13 | ## Run it as you wish :) 14 | # sudo docker run -d -e "VIRTUAL_HOST=metrello.home" -e "MONGO_URL=mongodb://172.17.0.3:27017/metrello-test" \ 15 | # -e "ROOT_URL=http://example.com" -p 5555:8080 -it grigio/metrello sh /meteor-run.sh 16 | -------------------------------------------------------------------------------- /client/views/modal/events.js: -------------------------------------------------------------------------------- 1 | Template.modal.events({ 2 | 'click .window-overlay': function(event, t) { 3 | // We only want to catch the event if the user click on the .window-overlay 4 | // div itself, not a child (ie, not the overlay window) 5 | if (event.target !== event.currentTarget) 6 | return; 7 | Utils.goBoardId(this.card.board()._id); 8 | event.preventDefault(); 9 | }, 10 | 'click .js-close-window': function(event) { 11 | Utils.goBoardId(this.card.board()._id); 12 | event.preventDefault(); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /client/views/widgets/rendered.js: -------------------------------------------------------------------------------- 1 | Template.membersWidget.rendered = function() { 2 | if (Meteor.user().isBoardMember()) { 3 | Utils.liveEvent('mouseover', function($this) { 4 | $this.find('.js-member').draggable({ 5 | appendTo: "body", 6 | helper: "clone", 7 | revert: "invalid", 8 | revertDuration: 150, 9 | snap: false, 10 | snapMode: "both" 11 | }); 12 | }); 13 | } 14 | }; 15 | 16 | Template.addMemberPopup.rendered = function() { 17 | // Input autofocus 18 | this.find('.search-with-spinner input').focus(); 19 | 20 | // resize widgets 21 | Utils.widgetsHeight(); 22 | }; 23 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | accounts-password 7 | audit-argument-checks 8 | markdown 9 | meteor-platform 10 | random 11 | reactive-dict 12 | 13 | dburles:collection-helpers 14 | fortawesome:fontawesome 15 | iron:router 16 | kenton:accounts-sandstorm 17 | linto:jquery-ui 18 | matb33:collection-hooks 19 | matteodem:easy-search 20 | mquandalle:moment 21 | mquandalle:jquery-textcomplete 22 | mquandalle:stylus 23 | ongoworks:speakingurl 24 | reywood:publish-composite 25 | seriousm:emoji-continued 26 | tap:i18n 27 | bengott:avatar 28 | aldeed:collection2 29 | idmontie:migrations 30 | cfs:standard-packages 31 | cfs:gridfs 32 | -------------------------------------------------------------------------------- /client/lib/i18n.js: -------------------------------------------------------------------------------- 1 | // We save the user language preference in the user profile, and use that to set 2 | // the language reactively. If the user is not connected we use the language 3 | // information provided by the browser, and default to english. 4 | // 5 | // XXX We don't handle momentjs translation, but it is probably a features for 6 | // tap:i18n, not for our application. See the following github issue: 7 | // 8 | // https://github.com/TAPevents/tap-i18n/issues/31 9 | 10 | Tracker.autorun(function() { 11 | var language; 12 | var currentUser = Meteor.user(); 13 | if (currentUser) { 14 | var language = currentUser.profile && currentUser.profile.language; 15 | } else { 16 | var language = navigator.language || navigator.userLanguage; 17 | } 18 | 19 | if (language) { 20 | TAPi18n.setLanguage(language); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /client/views/users/helpers.js: -------------------------------------------------------------------------------- 1 | Template.userAvatar.helpers({ 2 | userData: function() { 3 | if(!this.user){ 4 | this.user = Users.findOne(this.userId); 5 | } 6 | return this.user; 7 | }, 8 | memberType: function() { 9 | var userId = this.userId || this.user._id; 10 | var user = Users.findOne(userId); 11 | return user && user.isBoardAdmin() ? 'admin' : 'normal'; 12 | } 13 | }); 14 | 15 | Template.setLanguagePopup.helpers({ 16 | languages: function() { 17 | return _.map(TAPi18n.getLanguages(), function(lang, tag) { 18 | return { 19 | tag: tag, 20 | name: lang.name 21 | } 22 | }); 23 | }, 24 | isCurrentLanguage: function() { 25 | return this.tag === TAPi18n.getLanguage(); 26 | } 27 | }) 28 | 29 | Avatar.options = { 30 | fallbackType: 'initials' 31 | }; 32 | -------------------------------------------------------------------------------- /client/views/boards/rendered.js: -------------------------------------------------------------------------------- 1 | Template.board.rendered = function() { 2 | 3 | // update height add, update, remove resize board height. 4 | Boards.find().observe({ 5 | added: Utils.resizeHeight('.board-canvas', Utils.widgetsHeight), 6 | updated: Utils.resizeHeight('.board-canvas'), 7 | removed: Utils.resizeHeight('.board-canvas') 8 | }); 9 | 10 | // resize not update observe changed. 11 | jQuery(window).resize(Utils.resizeHeight('.board-canvas', Utils.widgetsHeight)); 12 | 13 | // if not is authenticated then show warning.. 14 | if (!Utils.is_authenticated()) Utils.Warning.open('Want to subscribe to these cards?'); 15 | 16 | // scroll Left getSession 17 | Utils.boardScrollLeft(); 18 | }; 19 | 20 | var jsAutofocus = function() { 21 | this.find('.js-autofocus').focus(); 22 | }; 23 | 24 | Template.boardChangeTitlePopup.rendered = jsAutofocus; 25 | Template.createBoardPopup.rendered = jsAutofocus; 26 | -------------------------------------------------------------------------------- /client/views/cards/routers.js: -------------------------------------------------------------------------------- 1 | Router.route('/boards/:boardId/:slug/:cardId', { 2 | name: 'Card', 3 | bodyClass: 'page-index chrome chrome-39 mac extra-large-window body-webkit-scrollbars body-board-view bgBoard window-up', 4 | waitOn: function() { 5 | var params = this.params; 6 | return [ 7 | 8 | // Update currentUser profile status 9 | Meteor.subscribe('connectUser'), 10 | 11 | // Board page list, cards members vs 12 | Meteor.subscribe('board', params.boardId, params.slug) 13 | ] 14 | }, 15 | action: function() { 16 | var params = this.params; 17 | this.render('board', { 18 | data: function() { 19 | return Boards.findOne(params.boardId); 20 | } 21 | }); 22 | this.render('cardModal', { 23 | to: 'modal', 24 | data: function() { 25 | return Cards.findOne(params.cardId); 26 | } 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /client/views/boards/helpers.js: -------------------------------------------------------------------------------- 1 | Template.boards.helpers({ 2 | boards: function() { 3 | return Boards.find({}, { 4 | sort: ["title"] 5 | }); 6 | }, 7 | starredBoards: function() { 8 | var cursor = Boards.find({ 9 | _id: { 10 | $in: Meteor.user().profile.starredBoards || [] 11 | } 12 | }, { 13 | sort: ["title"] 14 | }); 15 | return cursor.count() === 0 ? null : cursor; 16 | }, 17 | isStarred: function() { 18 | var user = Meteor.user(); 19 | return user && user.hasStarred(this._id); 20 | } 21 | }); 22 | 23 | Template.board.helpers({ 24 | isStarred: function() { 25 | var boardId = Boards.findOne()._id, 26 | user = Meteor.user(); 27 | return boardId && user && user.hasStarred(boardId); 28 | } 29 | }); 30 | 31 | Template.boardChangePermissionPopup.helpers({ 32 | check: function(perm) { 33 | return this.permission == perm; 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /server/migrations.js: -------------------------------------------------------------------------------- 1 | // Anytime you change the schema of one of the collection in a non-backward 2 | // compatible way you have to write a migration in this file using the following 3 | // API: 4 | // 5 | // Migrations.add(name, migrationCallback, optionalOrder); 6 | 7 | Migrations.add('board-background-color', function() { 8 | var defaultColor = '#16A085'; 9 | Boards.update({ 10 | background: { 11 | $exists: false 12 | } 13 | }, { 14 | $set: { 15 | background: { 16 | type: 'color', 17 | color: defaultColor 18 | } 19 | } 20 | }, { 21 | multi: true 22 | }); 23 | }); 24 | 25 | Migrations.add('lowercase-board-permission', function() { 26 | _.forEach(['Public', 'Private'], function(permission) { 27 | Boards.update( 28 | { permission: permission }, 29 | { $set: { permission: permission.toLowerCase() } }, 30 | { multi: true } 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /client/views/activities/events.js: -------------------------------------------------------------------------------- 1 | Template.cardActivities.events({ 2 | 'click .js-edit-action': function(event, t) { 3 | var $this = $(event.currentTarget), 4 | container = $this.parents('.phenom-comment'); 5 | 6 | // open and focus 7 | container.addClass('editing'); 8 | container.find('textarea').focus(); 9 | }, 10 | 'click .js-confirm-delete-action': function(event, t) { 11 | CardComments.remove(this._id); 12 | }, 13 | 'submit form': function(event, t) { 14 | var $this = $(event.currentTarget), 15 | container = $this.parents('.phenom-comment'), 16 | text = container.find('textarea'); 17 | if ($.trim(text.val())) { 18 | CardComments.update(this._id, { 19 | $set: { 20 | text: text.val() 21 | } 22 | }); 23 | 24 | // reset editing class 25 | $('.editing').removeClass('editing'); 26 | } 27 | event.preventDefault(); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /server/publications/users.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * To make this available on the client, use a reactive cursor, 4 | * such as by creating a publication on the server: 5 | */ 6 | Meteor.publish('connectUser', function() { 7 | var _this = this, 8 | user = Users.findOne(_this.userId); 9 | 10 | // if user then ready subscribe 11 | if (user) { 12 | 13 | // status offline then 14 | if (!user.profile.status) { 15 | 16 | // User profile.status update online 17 | Users.update(_this.userId, { $set: { 'profile.status': 'online' }}); 18 | } 19 | 20 | // user close subscribe onStop callback update user.profile.status 'offline' 21 | _this.onStop(function() { 22 | 23 | // update offline user 24 | Users.update(_this.userId, { $set: { 'profile.status': false }}); 25 | }); 26 | } 27 | 28 | 29 | // subscribe ready 30 | _this.ready(); 31 | }); 32 | 33 | Meteor.publish('profile', function(username) { 34 | check(username, String); 35 | return Users.find({ username: username }); 36 | }); 37 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## v.NEXT 2 | 3 | 4 | ## v0.1 5 | * Register, Login 6 | * Create, Update, Remove Board 7 | * Update Board name, Private, Public 8 | * Create List, Card 9 | * Remove List, Card 10 | * Drag and Drop List, Card 11 | * Update List Title, Card Description 12 | * Archive to Card 13 | * Board Members 14 | * Card Members 15 | * Add, remove, Board Members 16 | * Search Members and add Board 17 | * Profile page and edit Profile 18 | 19 | 20 | ## v0.2 21 | * Activities right widget and card detail 22 | * Template member and users pop fixed. 23 | * CardMembers subscribe fixed. 24 | * Boards page publish subscribe fixed 25 | * Livestamp added. 26 | 27 | ## v0.3 28 | * Card list description and vs icons. 29 | * Card comments add, remove, edit and Comment Activities 30 | * Right Widgets Activities list height fixed 31 | 32 | ## v0.4 33 | * Card Window actions show hide fixed 34 | * Wigdets show hide fixed 35 | 36 | ## v0.5 37 | * Comment remove can not be deleted in review activities. 38 | 39 | ## v0.5.1 40 | * remove Boards fixed. 41 | 42 | ## v0.6 43 | * Card Detail Page 44 | * Boards and Cards helper add absoluteUrl method. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Yasar Icli, Maxime Quandalle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/views/users/routers.js: -------------------------------------------------------------------------------- 1 | Router.route('/login', { 2 | name: 'Login', 3 | template: 'login', 4 | layoutTemplate: 'StaticLayout', 5 | bodyClass: 'account-page', 6 | redirectLoggedInUsers: true 7 | }); 8 | 9 | Router.route('/signup', { 10 | name: 'Signup', 11 | template: 'signup', 12 | layoutTemplate: 'StaticLayout', 13 | bodyClass: 'account-page', 14 | redirectLoggedInUsers: true 15 | }); 16 | 17 | Router.route('/profile/:username', { 18 | name: 'Profile', 19 | template: 'profile', 20 | bodyClass: 'page-index chrome chrome-39 mac large-window body-webkit-scrollbars tabbed-page', 21 | waitOn: function() { 22 | return Meteor.subscribe('profile', this.params.username); 23 | }, 24 | data: function() { 25 | var params = this.params; 26 | return { 27 | profile: function() { 28 | return Users.findOne({ username: params.username }); 29 | } 30 | } 31 | } 32 | }); 33 | 34 | Router.route('/settings', { 35 | name: 'Settings', 36 | template: 'settings', 37 | layoutTemplate: 'AuthLayout', 38 | bodyClass: 'page-index chrome chrome-39 mac large-window body-webkit-scrollbars tabbed-page' 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibreBoard [![Build Status][travis-status]][travis-link] 2 | 3 | LibreBoard is an open-source *kanban* board that let you organize things in 4 | cards, and cards in lists. You can use it alone, or with your team and family 5 | thanks to our real-time synchronisation feature. Libreboard is a land of liberty 6 | and you can implement all sort of workflows on it using tags, comments, member 7 | assignation, and many more. 8 | 9 | [![Our roadmap is self-hosted on LibreBoard][thumbnail]][roadmap] 10 | 11 | Since it is a free software, you don’t have to trust us with your data and can 12 | install LibreBoard on your own computer or server. In fact we encourage you to 13 | do that by providing one-click installation for the 14 | [Sandstorm](https://sandstorm.io) platform and verified 15 | [Docker](https://www.docker.com) images. 16 | 17 | LibreBoard is released under the very permissive [MIT license](LICENSE), and 18 | made with [Meteor](https://www.meteor.com). 19 | 20 | [Our roadmap is self-hosted on LibreBoard][roadmap] 21 | 22 | [travis-status]: https://travis-ci.org/libreboard/libreboard.svg 23 | [travis-link]: https://travis-ci.org/libreboard/libreboard.svg 24 | [thumbnail]: http://i.imgur.com/PMAh7qR.png 25 | [roadmap]: http://libreboard.com/boards/MeSsFJaSqeuo9M6bs/libreboard-roadmap 26 | -------------------------------------------------------------------------------- /client/views/cards/helpers.js: -------------------------------------------------------------------------------- 1 | Template.addCardForm.helpers({}); 2 | 3 | Template.cardMembersPopup.helpers({ 4 | isCardMember: function() { 5 | var cardId = Template.parentData().card._id; 6 | var cardMembers = Cards.findOne(cardId).members || []; 7 | return _.contains(cardMembers, this.userId); 8 | }, 9 | user: function() { 10 | return Users.findOne(this.userId); 11 | } 12 | }); 13 | 14 | Template.cardLabelsPopup.helpers({ 15 | isLabelSelected: function(cardId) { 16 | return _.contains(Cards.findOne(cardId).labelIds, this._id); 17 | } 18 | }); 19 | 20 | var labelColors = ['green', 'yellow', 'orange', 'red', 'purple', 'blue', 'sky', 21 | 'lime', 'pink', 'black']; 22 | 23 | Template.createLabelPopup.helpers({ 24 | // This is the default color for a new label. We search the first color that 25 | // is not already used in the board (although it's not a problem if two 26 | // labels have the same color) 27 | defaultColor: function() { 28 | var usedColors = _.pluck(this.card.board().labels, 'color'); 29 | var availableColors = _.difference(labelColors, usedColors); 30 | return availableColors.length > 1 ? availableColors[0] : 'green'; 31 | } 32 | }); 33 | 34 | Template.formLabel.helpers({ 35 | labels: function() { 36 | return _.map(labelColors, function(color) { 37 | return { color: color, name: '' }; 38 | }); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /client/views/main/rendered.js: -------------------------------------------------------------------------------- 1 | Template.editor.rendered = function() { 2 | this.$('textarea').textcomplete([ 3 | { // emojies 4 | match: /\B:([\-+\w]*)$/, 5 | search: function (term, callback) { 6 | callback($.map(Emoji.values, function (emoji) { 7 | return emoji.indexOf(term) === 0 ? emoji : null; 8 | })); 9 | }, 10 | template: function (value) { 11 | return '' + value; 12 | }, 13 | replace: function (value) { 14 | return ':' + value + ':'; 15 | }, 16 | index: 1 17 | }, 18 | { // user mentions 19 | match: /\B@(\w*)$/, 20 | search: function (term, callback) { 21 | var currentBoard = Boards.findOne(Router.current().params.boardId); 22 | callback($.map(currentBoard.members, function (member) { 23 | var username = Users.findOne(member.userId).username; 24 | return username.indexOf(term) === 0 ? username : null; 25 | })); 26 | }, 27 | template: function (value) { 28 | return value; 29 | }, 30 | replace: function (username) { 31 | return '@' + username + ' '; 32 | }, 33 | index: 1 34 | } 35 | ]); 36 | } 37 | -------------------------------------------------------------------------------- /collections/activities.js: -------------------------------------------------------------------------------- 1 | // Activities don't need a schema because they are always set from the a trusted 2 | // environment - the server - and there is no risk that a user change the logic 3 | // we use with this collection. Moreover using a schema for this collection 4 | // would be difficult (different activities have different fields) and wouldn't 5 | // bring any direct advantage. 6 | // 7 | // XXX The activities API is not so nice and need some functionalities. For 8 | // instance if a user archive a card, and un-archive it a few seconds later 9 | // we should remove both activities assuming it was an error the user decided 10 | // to revert. 11 | Activities = new Mongo.Collection('activities'); 12 | 13 | Activities.helpers({ 14 | board: function() { 15 | return Boards.findOne(this.boardId); 16 | }, 17 | user: function() { 18 | return Users.findOne(this.userId); 19 | }, 20 | member: function() { 21 | return Users.findOne(this.memberId); 22 | }, 23 | list: function() { 24 | return Lists.findOne(this.listId); 25 | }, 26 | oldList: function() { 27 | return Lists.findOne(this.oldListId); 28 | }, 29 | card: function() { 30 | return Cards.findOne(this.cardId); 31 | }, 32 | comment: function() { 33 | return CardComments.findOne(this.commentId); 34 | }, 35 | attachment: function() { 36 | return Attachments.findOne(this.attachmentId); 37 | } 38 | }); 39 | 40 | Activities.before.insert(function(userId, doc) { 41 | doc.createdAt = new Date(); 42 | }); 43 | -------------------------------------------------------------------------------- /client/views/lists/rendered.js: -------------------------------------------------------------------------------- 1 | Template.lists.rendered = function() { 2 | var _this = this, 3 | data = this.data, 4 | lists = _this.$(".lists"); 5 | 6 | if (Meteor.user().isBoardMember()) { 7 | lists.sortable({ 8 | connectWith: ".lists", 9 | handle: ".list-header", 10 | tolerance: 'pointer', 11 | appendTo: 'body', 12 | helper: "clone", 13 | items: '.list:not(.add-list)', 14 | placeholder: 'list placeholder', 15 | start: function (event, ui) { 16 | $('.list.placeholder').height(ui.item.height()); 17 | }, 18 | stop: function(event, ui) { 19 | lists.find('.list:not(.add-list)').each(function(i, list) { 20 | var data = Blaze.getData(list); 21 | Lists.update(data._id, { 22 | $set: { 23 | sort: i 24 | } 25 | }); 26 | }); 27 | } 28 | }).disableSelection(); 29 | 30 | // If there is no data in the board (ie, no lists) we autofocus the list 31 | // creation form by clicking on the corresponding element. 32 | if (data.board.lists().count() === 0) { 33 | _this.$("#AddListForm .js-open-add-list").click(); 34 | } 35 | } 36 | 37 | // update height add, update, remove resize board height. 38 | Lists.find().observe({ 39 | added: Utils.resizeHeight('.board-canvas'), 40 | updated: Utils.resizeHeight('.board-canvas'), 41 | removed: Utils.resizeHeight('.board-canvas') 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /client/views/widgets/helpers.js: -------------------------------------------------------------------------------- 1 | var currentBoard = function() { 2 | return Boards.findOne(Router.current().params.boardId); 3 | } 4 | 5 | var widgetTitles = { 6 | filter: 'filter-cards', 7 | background: 'change-background' 8 | }; 9 | 10 | Template.boardWidgets.helpers({ 11 | currentWidget: function() { 12 | return Session.get('currentWidget') + 'Widget'; 13 | }, 14 | currentWidgetTitle: function() { 15 | return TAPi18n.__(widgetTitles[Session.get('currentWidget')]); 16 | } 17 | }); 18 | 19 | Template.addMemberPopup.helpers({ 20 | isBoardMember: function() { 21 | var user = Users.findOne(this._id); 22 | return user && user.isBoardMember(); 23 | } 24 | }); 25 | 26 | Template.backgroundWidget.helpers({ 27 | backgroundColors: function() { 28 | return DefaultBoardBackgroundColors; 29 | } 30 | }); 31 | 32 | Template.memberPopup.helpers({ 33 | user: function() { 34 | return Users.findOne(this.userId); 35 | }, 36 | memberType: function() { 37 | var type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal'; 38 | return TAPi18n.__(type).toLowerCase(); 39 | } 40 | }); 41 | 42 | Template.removeMemberPopup.helpers({ 43 | user: function() { 44 | return Users.findOne(this.userId) 45 | }, 46 | board: function() { 47 | return currentBoard(); 48 | } 49 | }); 50 | 51 | Template.changePermissionsPopup.helpers({ 52 | isAdmin: function() { 53 | return this.user.isBoardAdmin(); 54 | }, 55 | isLastAdmin: function() { 56 | if (! this.user.isBoardAdmin()) 57 | return false; 58 | var nbAdmins = _.where(currentBoard().members, { isAdmin: true }).length; 59 | return nbAdmins === 1; 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /sandstorm.js: -------------------------------------------------------------------------------- 1 | // Sandstorm context is detected using the METEOR_SETTINGS environment variable 2 | // in the package definition. 3 | // XXX We could probably rely on the user object to detect sandstorm usage. 4 | var isSandstorm = Meteor.settings && 5 | Meteor.settings.public && Meteor.settings.public.sandstorm; 6 | 7 | // In sandstorm we only have one board per sandstorm instance. Since we want to 8 | // keep most of our code unchanged, we simply hard-code a board `_id` and 9 | // redirect the user to this particular board. 10 | var sandstormBoard = { 11 | _id: "sandstorm", 12 | title: "LibreBoard" 13 | }; 14 | 15 | // On the first launch of the instance a user is automatically created thanks to 16 | // the `accounts-sandstorm` package. After its creation we insert the unique 17 | // board document. 18 | if (isSandstorm && Meteor.isServer) { 19 | Users.after.insert(function(userId, doc) { 20 | Boards.insert({ 21 | _id: sandstormBoard._id, 22 | // XXX should be shared with the instance name 23 | title: sandstormBoard.title, 24 | userId: doc._id 25 | }); 26 | }); 27 | } 28 | 29 | // On the client, redirect the user to the hard-coded board. On the first launch 30 | // the user will be redirected to the board before its creation. But that's not 31 | // a problem thanks to the reactive board publication. 32 | if (isSandstorm && Meteor.isClient) { 33 | Router.go('Board', { 34 | boardId: sandstormBoard._id, 35 | slug: getSlug(sandstormBoard.title) 36 | }); 37 | } 38 | 39 | // We use this blaze helper in the UI to hide some template that does not make 40 | // sense in the context of sandstorm, like board staring, board archiving, user 41 | // name edition, etc. 42 | Blaze.registerHelper("isSandstorm", function() { 43 | return isSandstorm; 44 | }); 45 | -------------------------------------------------------------------------------- /collections/users.js: -------------------------------------------------------------------------------- 1 | Users = Meteor.users; 2 | 3 | // Search subscribe mongodb fields ['username', 'profile.name'] 4 | Users.initEasySearch(['username', 'profile.name'], { 5 | use: 'mongo-db' 6 | }); 7 | 8 | 9 | // HELPERS 10 | Users.helpers({ 11 | boards: function() { 12 | return Boards.find({ userId: this._id }); 13 | }, 14 | hasStarred: function(boardId) { 15 | return this.profile.starredBoards && _.contains(this.profile.starredBoards, boardId); 16 | }, 17 | isBoardMember: function() { 18 | var board = Boards.findOne(Router.current().params.boardId); 19 | return _.contains(_.pluck(board.members, 'userId'), this._id); 20 | }, 21 | isBoardAdmin: function() { 22 | var board = Boards.findOne(Router.current().params.boardId); 23 | return this.isBoardMember(board) && _.where(board.members, {userId: this._id})[0].isAdmin; 24 | } 25 | }); 26 | 27 | 28 | // BEFORE HOOK 29 | Users.before.insert(function (userId, doc) { 30 | 31 | // connect profile.status default 32 | doc.profile.status = 'offline'; 33 | 34 | // slugify to username 35 | doc.username = getSlug(doc.profile.name, ''); 36 | }); 37 | 38 | 39 | // AFTER HOOK 40 | isServer(function() { 41 | Users.after.insert(function(userId, doc) { 42 | var ExampleBoard = { 43 | title: 'Welcome Board', 44 | userId: doc._id, 45 | permission: 'private' // Private || Public 46 | }; 47 | 48 | // Welcome Board insert and list, card :) 49 | Boards.insert(ExampleBoard, function(err, boardId) { 50 | 51 | // lists 52 | _.forEach(['Basics', 'Advanced'], function(title) { 53 | var list = { 54 | title: title, 55 | boardId: boardId, 56 | userId: ExampleBoard.userId 57 | }; 58 | 59 | // insert List 60 | Lists.insert(list); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /collections/attachments.js: -------------------------------------------------------------------------------- 1 | Attachments = new FS.Collection("attachments", { 2 | stores: [ 3 | // XXX Add a new store for cover thumbnails so we don't load big images 4 | // in the general board view 5 | new FS.Store.GridFS("attachments") 6 | ] 7 | }); 8 | 9 | Attachments.allow({ 10 | insert: function(userId, doc) { 11 | return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); 12 | }, 13 | update: function(userId, doc) { 14 | return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); 15 | }, 16 | remove: function(userId, doc) { 17 | return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); 18 | }, 19 | // We authorize the attachment download either: 20 | // - if the board is public, everyone (even unconnected) can download it 21 | // - if the board is private, only board members can download it 22 | // 23 | // XXX We have a bug with the userId verification: 24 | // 25 | // https://github.com/CollectionFS/Meteor-CollectionFS/issues/449 26 | // 27 | download: function(userId, doc) { 28 | var query = { 29 | $or: [ 30 | { 'members.userId': userId }, 31 | { permission: 'public' } 32 | ] 33 | }; 34 | return !!Boards.findOne(doc.boardId, query); 35 | }, 36 | fetch: ['boardId'] 37 | }); 38 | 39 | // XXX Enforce a schema for the Attachments CollectionFS 40 | 41 | Attachments.files.before.insert(function(userId, doc) { 42 | var file = new FS.File(doc); 43 | doc.userId = userId; 44 | 45 | if (file.isImage()) { 46 | doc.cover = true; 47 | } 48 | }); 49 | 50 | isServer(function() { 51 | Attachments.files.after.insert(function(userId, doc) { 52 | Activities.insert({ 53 | type: 'card', 54 | activityType: "addAttachment", 55 | attachmentId: doc._id, 56 | boardId: doc.boardId, 57 | cardId: doc.cardId, 58 | userId: userId 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /client/views/cards/rendered.js: -------------------------------------------------------------------------------- 1 | Template.cards.rendered = function() { 2 | var _this = this, 3 | cards = _this.$(".cards"); 4 | 5 | if (Meteor.user().isBoardMember()) { 6 | cards.sortable({ 7 | connectWith: ".js-sortable", 8 | tolerance: 'pointer', 9 | appendTo: 'body', 10 | helper: "clone", 11 | items: '.list-card:not(.placeholder, .hide, .js-composer)', 12 | placeholder: 'list-card placeholder', 13 | start: function (event, ui) { 14 | $('.list-card.placeholder').height(ui.item.height()); 15 | }, 16 | stop: function(event, ui) { 17 | var data = new Utils.getCardData(ui.item); 18 | Cards.update(data.cardId, { 19 | $set: { 20 | listId: data.listId, 21 | sort: data.sort 22 | } 23 | }); 24 | } 25 | }).disableSelection(); 26 | 27 | Utils.liveEvent('mouseover', function($this) { 28 | $this.find('.js-member-droppable').droppable({ 29 | hoverClass: "draggable-hover-card", 30 | accept: '.js-member', 31 | drop: function(event, ui) { 32 | var memberId = Blaze.getData(ui.draggable.get(0)).userId; 33 | var cardId = Blaze.getData(this)._id; 34 | Cards.update(cardId, {$addToSet: { members: memberId}}); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | // update height add, update, remove resize board height. 41 | Cards.find().observe({ 42 | added: Utils.resizeHeight('.board-canvas'), 43 | updated: Utils.resizeHeight('.board-canvas'), 44 | removed: Utils.resizeHeight('.board-canvas') 45 | }); 46 | }; 47 | 48 | Template.formLabel.rendered = function() { 49 | this.find('.js-autofocus').focus(); 50 | }; 51 | 52 | Template.cardMorePopup.rendered = function() { 53 | this.find('.js-autoselect').select(); 54 | }; 55 | -------------------------------------------------------------------------------- /client/views/boards/routers.js: -------------------------------------------------------------------------------- 1 | Router.route('/boards', { 2 | name: 'Boards', 3 | template: 'boards', 4 | bodyClass: 'page-index large-window tabbed-page', 5 | authenticated: 'Login', 6 | waitOn: function() { 7 | return Meteor.subscribe('boards'); 8 | } 9 | }); 10 | 11 | Router.route('/boards/:boardId/:slug', { 12 | name: 'Board', 13 | template: 'board', 14 | bodyClass: 'page-index chrome chrome-39 mac extra-large-window body-webkit-scrollbars body-board-view bgBoard', 15 | onAfterAction: function() { 16 | Session.set('sidebarIsOpen', true); 17 | Session.set('currentWidget', 'home'); 18 | Session.set('menuWidgetIsOpen', false); 19 | }, 20 | waitOn: function() { 21 | var params = this.params; 22 | 23 | return [ 24 | 25 | // Update currentUser profile status 26 | Meteor.subscribe('connectUser'), 27 | 28 | // Board page list, cards members vs 29 | Meteor.subscribe('board', params.boardId, params.slug) 30 | ] 31 | }, 32 | data: function() { 33 | return Boards.findOne(this.params.boardId); 34 | } 35 | }); 36 | 37 | // Reactively set the color of the page from the color of the current board. 38 | Meteor.startup(function() { 39 | Tracker.autorun(function() { 40 | var currentRoute = Router.current(); 41 | // We have to be very defensive here because we have no idea what the 42 | // state of the application is, so we have to test existence of any 43 | // property we want to use. 44 | // XXX There is one feature of coffeescript that rely shine in this kind 45 | // of code: `currentRoute?.params?.boardId` -- concise and clear. 46 | var currentBoard = Boards.findOne(currentRoute && 47 | currentRoute.params && 48 | currentRoute.params.boardId); 49 | if (currentBoard && 50 | currentBoard.background && 51 | currentBoard.background.type === "color") { 52 | $(document.body).css({ 53 | backgroundColor: currentBoard.background.color 54 | }); 55 | } else { 56 | $(document.body).css({ backgroundColor: '' }); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /client/views/main/routers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * If you want to use a default layout template for all routes you can 3 | * configure a global Router option. 4 | */ 5 | Router.configure({ 6 | loadingTemplate: 'loading', 7 | notFoundTemplate: 'notfound', 8 | 9 | yieldRegions: { 10 | '': { 11 | to: 'modal' 12 | } 13 | }, 14 | 15 | /* 16 | * onBeforeAction hooks now require you to call this.next(), 17 | * and no longer take a pause() argument. So the default behaviour is reversed. 18 | * ClassMapper body add, remove class. 19 | */ 20 | onBeforeAction: function(pause) { 21 | var body = $('body'), 22 | options = this.route.options, 23 | bodyClass = options["bodyClass"], 24 | authenticate = options['authenticated'], 25 | redirectLoggedInUsers = options['redirectLoggedInUsers']; 26 | 27 | // redirect logged in users to Boards view when they try to open Login or Signup views 28 | if (Meteor.user() && redirectLoggedInUsers) { 29 | // redirect 30 | this.redirect('Boards'); 31 | return; 32 | } 33 | 34 | // authenticated 35 | if (!Meteor.user() && authenticate) { 36 | 37 | // redirect 38 | this.redirect(authenticate); 39 | 40 | // options authenticate not next(). 41 | return; 42 | } 43 | 44 | // Remove class attribute body 45 | body.removeAttr('class'); 46 | 47 | // if klass iron router name currentRouter then add Class 48 | if (bodyClass) body.addClass(bodyClass); 49 | 50 | // reset default sessions 51 | Session.set('error', false); 52 | Session.set('warning', false); 53 | 54 | Popup.close(); 55 | 56 | // Layout template found then set render this.route options layout. 57 | if (!options.layoutTemplate) { 58 | 59 | // if user undefined then layout render 60 | if (!Meteor.user()) this.layout('layout'); 61 | 62 | // user found then AuthLayout render 63 | else this.layout('AuthLayout'); 64 | } 65 | 66 | // Next 67 | this.next(); 68 | } 69 | }); 70 | 71 | Router.route('/', { 72 | name: 'Home', 73 | template: 'home', 74 | layoutTemplate: 'LandingLayout' 75 | }); 76 | -------------------------------------------------------------------------------- /sandstorm-pkgdef.capnp: -------------------------------------------------------------------------------- 1 | @0xa5275bd3ad124e12; 2 | 3 | using Spk = import "/sandstorm/package.capnp"; 4 | # This imports: 5 | # $SANDSTORM_HOME/latest/usr/include/sandstorm/package.capnp 6 | # Check out that file to see the full, documented package definition format. 7 | 8 | const pkgdef :Spk.PackageDefinition = ( 9 | # The package definition. Note that the spk tool looks specifically for the 10 | # "pkgdef" constant. 11 | 12 | id = "m86q05rdvj14yvn78ghaxynqz7u2svw6rnttptxx49g1785cdv1h", 13 | # Your app ID is actually its public key. The private key was placed in 14 | # your keyring. All updates must be signed with the same key. 15 | 16 | manifest = ( 17 | # This manifest is included in your app package to tell Sandstorm 18 | # about your app. 19 | 20 | appVersion = 0, # Increment this for every release. 21 | 22 | actions = [ 23 | # Define your "new document" handlers here. 24 | ( title = (defaultText = "New LibreBoard"), 25 | command = .myCommand 26 | # The command to run when starting for the first time. (".myCommand" 27 | # is just a constant defined at the bottom of the file.) 28 | ) 29 | ], 30 | 31 | continueCommand = .myCommand 32 | # This is the command called to start your app back up after it has been 33 | # shut down for inactivity. Here we're using the same command as for 34 | # starting a new instance, but you could use different commands for each 35 | # case. 36 | ), 37 | 38 | sourceMap = ( 39 | # The following directories will be copied into your package. 40 | searchPath = [ 41 | ( sourcePath = ".meteor-spk/deps" ), 42 | ( sourcePath = ".meteor-spk/bundle" ) 43 | ] 44 | ), 45 | 46 | alwaysInclude = [ "." ] 47 | # This says that we always want to include all files from the source map. 48 | # (An alternative is to automatically detect dependencies by watching what 49 | # the app opens while running in dev mode. To see what that looks like, 50 | # run `spk init` without the -A option.) 51 | ); 52 | 53 | const myCommand :Spk.Manifest.Command = ( 54 | # Here we define the command used to start up your server. 55 | argv = ["/sandstorm-http-bridge", "4000", "--", "node", "start.js"], 56 | environ = [ 57 | # Note that this defines the *entire* environment seen by your app. 58 | (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"), 59 | (key = "METEOR_SETTINGS", value = "{\"public\": {\"sandstorm\": true}}") 60 | ] 61 | ); 62 | -------------------------------------------------------------------------------- /collections/lists.js: -------------------------------------------------------------------------------- 1 | Lists = new Mongo.Collection('lists'); 2 | 3 | Lists.attachSchema(new SimpleSchema({ 4 | title: { 5 | type: String 6 | }, 7 | archived: { 8 | type: Boolean, 9 | defaultValue: false 10 | }, 11 | boardId: { 12 | type: String 13 | }, 14 | createdAt: { 15 | type: Date, 16 | denyUpdate: true, 17 | autoValue: function() { 18 | if (this.isInsert) 19 | return new Date(); 20 | } 21 | }, 22 | sort: { 23 | type: Number, 24 | decimal: true, 25 | // XXX We should probably provide a default 26 | optional: true 27 | }, 28 | updatedAt: { 29 | type: Date, 30 | denyInsert: true, 31 | optional: true, 32 | autoValue: function() { 33 | if (this.isUpdate) 34 | return new Date(); 35 | } 36 | } 37 | })); 38 | 39 | Lists.allow({ 40 | insert: function(userId, doc) { 41 | return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); 42 | }, 43 | update: function(userId, doc) { 44 | return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); 45 | }, 46 | remove: function(userId, doc) { 47 | return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); 48 | }, 49 | fetch: ['boardId'] 50 | }); 51 | 52 | 53 | Lists.helpers({ 54 | cards: function() { 55 | return Cards.find(_.extend(Filter.getMongoSelector(), { 56 | listId: this._id, 57 | archived: false 58 | }), { sort: ['sort'] }); 59 | }, 60 | board: function() { 61 | return Boards.findOne(this.boardId); 62 | } 63 | }); 64 | 65 | // HOOKS 66 | Lists.hookOptions.after.update = { fetchPrevious: false }; 67 | 68 | isServer(function() { 69 | Lists.after.insert(function(userId, doc) { 70 | Activities.insert({ 71 | type: 'list', 72 | activityType: "createList", 73 | boardId: doc.boardId, 74 | listId: doc._id, 75 | userId: userId 76 | }); 77 | }); 78 | 79 | Lists.after.update(function(userId, doc) { 80 | if (doc.archived) { 81 | Activities.insert({ 82 | type: 'list', 83 | activityType: "archivedList", 84 | listId: doc._id, 85 | boardId: doc.boardId, 86 | userId: userId 87 | }); 88 | } 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /client/views/boards/events.js: -------------------------------------------------------------------------------- 1 | var toggleBoardStar = function(boardId) { 2 | var queryType = Meteor.user().hasStarred(boardId) ? '$pull' : '$addToSet'; 3 | var query = {}; 4 | query[queryType] = { 5 | 'profile.starredBoards': boardId 6 | }; 7 | Meteor.users.update(Meteor.userId(), query); 8 | }; 9 | 10 | Template.boards.events({ 11 | 'click .js-star-board': function(event, t) { 12 | toggleBoardStar(this._id); 13 | event.preventDefault(); 14 | } 15 | }); 16 | 17 | Template.board.events({ 18 | 'click .js-star-board': function(event, t) { 19 | toggleBoardStar(this._id); 20 | }, 21 | 'click .js-rename-board:not(.no-edit)': Popup.open('boardChangeTitle'), 22 | 'click #permission-level:not(.no-edit)': Popup.open('boardChangePermission'), 23 | 'click .js-filter-cards-indicator': function(event) { 24 | Session.set('currentWidget', 'filter'); 25 | event.preventDefault(); 26 | }, 27 | 'click .js-filter-card-clear': function(event) { 28 | Filter.reset(); 29 | event.stopPropagation(); 30 | } 31 | }); 32 | 33 | Template.createBoardPopup.events({ 34 | 'submit #CreateBoardForm': function(event, t) { 35 | var title = t.$('#boardNewTitle'); 36 | 37 | // trim value title 38 | if ($.trim(title.val())) { 39 | // İnsert Board title 40 | var boardId = Boards.insert({ 41 | title: title.val(), 42 | permission : 'public' 43 | }); 44 | 45 | // Go to Board _id 46 | Utils.goBoardId(boardId); 47 | } 48 | event.preventDefault(); 49 | } 50 | }); 51 | 52 | Template.boardChangeTitlePopup.events({ 53 | 'submit #ChangeBoardTitleForm': function(event, t) { 54 | var title = t.$('.js-board-name').val().trim(); 55 | if (title) { 56 | Boards.update(this._id, { 57 | $set: { 58 | title: title 59 | } 60 | }); 61 | Popup.close(); 62 | } 63 | event.preventDefault(); 64 | } 65 | }); 66 | 67 | Template.boardChangePermissionPopup.events({ 68 | 'click .js-select': function(event, t) { 69 | var $this = $(event.currentTarget), 70 | permission = $this.attr('name'); 71 | 72 | Boards.update(this._id, { 73 | $set: { 74 | permission: permission 75 | } 76 | }); 77 | Popup.close(); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /client/views/main/helpers.js: -------------------------------------------------------------------------------- 1 | var Helpers = { 2 | error: function() { 3 | return Session.get('error'); 4 | }, 5 | toLowerCase: function(text) { 6 | return text && text.toLowerCase(); 7 | }, 8 | toUpperCase: function(text) { 9 | return text && text.toUpperCase(); 10 | }, 11 | firstChar: function(text) { 12 | return text && text[0].toUpperCase(); 13 | }, 14 | session: function(prop) { 15 | return Session.get(prop); 16 | }, 17 | isTrue: function(a, b) { 18 | return a == b; 19 | }, 20 | getUser: function(userId) { 21 | return Users.findOne(userId); 22 | } 23 | }; 24 | 25 | Template.warning.helpers({ 26 | warning: function() { 27 | return Utils.Warning.get(); 28 | } 29 | }); 30 | 31 | // Register all Helpers 32 | _.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); }); 33 | 34 | // XXX I believe we should compute a HTML rendered field on the server that 35 | // would handle markdown, emojies and user mentions. We can simply have two 36 | // fields, one source, and one compiled version (in HTML) and send only the 37 | // compiled version to most users -- who don't need to edit. 38 | // In the meantime, all the transformation are done on the client using the 39 | // Blaze API. 40 | var at = HTML.CharRef({html: '@', str: '@'}); 41 | Blaze.Template.registerHelper('mentions', new Template('mentions', function() { 42 | var view = this; 43 | var content = Blaze.toHTML(view.templateContentBlock); 44 | var currentBoard = Boards.findOne(Router.current().params.boardId); 45 | var knowedUsers = _.map(currentBoard.members, function(member) { 46 | member.username = Users.findOne(member.userId).username; 47 | return member; 48 | }); 49 | 50 | var mentionRegex = /\B@(\w*)/gi; 51 | var currentMention, knowedUser, href, linkClass, linkValue, link; 52 | while (currentMention = mentionRegex.exec(content)) { 53 | 54 | knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] }); 55 | if (! knowedUser) 56 | continue; 57 | 58 | linkValue = [' ', at, knowedUser.username]; 59 | href = Router.url('Profile', { username: knowedUser.username }); 60 | linkClass = 'atMention' + (knowedUser.userId === Meteor.userId() ? ' me' : ''); 61 | link = HTML.A({ href: href, "class": linkClass }, linkValue); 62 | 63 | content = content.replace(currentMention[0], Blaze.toHTML(link)); 64 | } 65 | 66 | return HTML.Raw(content); 67 | })); 68 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We’re glad you’re interested in helping the LibreBoard project! We welcome bug 4 | reports, enhancement ideas, and pull requests, in our Github bug tracker. Before 5 | opening a new thread please verify that your issue hasn’t already been reported. 6 | 7 | 8 | 9 | ## Translations 10 | 11 | You are encouraged to translate — or improve the translation of — LibreBoard in 12 | your locale language. For that purpose we rely on 13 | [Transifex](https://www.transifex.com/projects/p/libreboard). So the first step 14 | is to create a Transifex account if you don’t have one already. You can then 15 | send a request to join one of the translation teams. If there is no team for 16 | your language, contact us by email or open a Github issue and we will create a 17 | new one. 18 | 19 | Once you are in a team you can start translating the application. Please take a 20 | look at the glossary so you can agree with other (present and future) 21 | contributors on words to use to translate key concepts in the application like 22 | “boards” and “cards”. 23 | 24 | The original application is written in English, and if you want to contribute to 25 | the application itself, you are asked to fill the `i18n/en.i18n.json` file. When 26 | you do that the new strings of text to translate automatically appears on 27 | Transifex to be translated (the refresh may take a few hours). 28 | 29 | We pull all translations from Transifex before every new LibreBoard release 30 | candidate, ask the translators to review the app, and pull all translations 31 | again for the final release. 32 | 33 | ## Installation 34 | 35 | LibreBoard is made with [Meteor](https://www.meteor.com). Thus the easiest way 36 | to start hacking is by installing the framework, cloning the git repository, and 37 | launching the application: 38 | 39 | ```bash 40 | $ curl https://install.meteor.com/ | sh # On Mac or Linux 41 | $ git clone https://github.com/libreboard/libreboard.git 42 | $ cd libreboard 43 | $ meteor 44 | ``` 45 | 46 | As for any Meteor application, LibreBoard is automatically refreshed when you 47 | change any file of the source code, just play with it to see how it behaves! 48 | 49 | ## Style guide 50 | 51 | We follow the 52 | [meteor style guide](https://github.com/meteor/meteor/wiki/Meteor-Style-Guide) 53 | with only one modification, which is that we indent using 4 spaces (spaces, not 54 | literal tabs). 55 | 56 | Please read the meteor style guide before making any significant contribution. 57 | 58 | ## Code organisation 59 | 60 | TODO 61 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.1.3 2 | accounts-password@1.0.5 3 | aldeed:collection2@2.3.1 4 | aldeed:simple-schema@1.3.0 5 | application-configuration@1.0.4 6 | audit-argument-checks@1.0.2 7 | autoupdate@1.1.4 8 | base64@1.0.2 9 | bengott:avatar@0.7.3 10 | binary-heap@1.0.2 11 | blaze@2.0.4 12 | blaze-tools@1.0.2 13 | boilerplate-generator@1.0.2 14 | callback-hook@1.0.2 15 | cfs:access-point@0.1.43 16 | cfs:base-package@0.0.27 17 | cfs:collection@0.5.3 18 | cfs:collection-filters@0.2.3 19 | cfs:data-man@0.0.4 20 | cfs:file@0.1.15 21 | cfs:gridfs@0.0.27 22 | cfs:http-methods@0.0.27 23 | cfs:http-publish@0.0.13 24 | cfs:power-queue@0.9.11 25 | cfs:reactive-list@0.0.9 26 | cfs:reactive-property@0.0.4 27 | cfs:standard-packages@0.5.3 28 | cfs:storage-adapter@0.1.1 29 | cfs:tempstore@0.1.3 30 | cfs:upload-http@0.0.19 31 | cfs:worker@0.1.3 32 | check@1.0.3 33 | coffeescript@1.0.5 34 | dburles:collection-helpers@1.0.2 35 | ddp@1.0.13 36 | deps@1.0.6 37 | ejson@1.0.5 38 | email@1.0.5 39 | fastclick@1.0.2 40 | follower-livedata@1.0.3 41 | fortawesome:fontawesome@4.2.0_2 42 | geojson-utils@1.0.2 43 | html-tools@1.0.3 44 | htmljs@1.0.3 45 | http@1.0.9 46 | id-map@1.0.2 47 | idmontie:migrations@1.0.0 48 | iron:controller@1.0.7 49 | iron:core@1.0.7 50 | iron:dynamic-template@1.0.7 51 | iron:layout@1.0.7 52 | iron:location@1.0.7 53 | iron:middleware-stack@1.0.7 54 | iron:router@1.0.7 55 | iron:url@1.0.7 56 | jparker:crypto-core@0.1.0 57 | jparker:crypto-md5@0.1.1 58 | jparker:gravatar@0.3.1 59 | jquery@1.0.2 60 | json@1.0.2 61 | kenton:accounts-sandstorm@0.1.3 62 | launch-screen@1.0.1 63 | less@1.0.12 64 | linto:jquery-ui@1.11.2 65 | livedata@1.0.12 66 | localstorage@1.0.2 67 | logging@1.0.6 68 | markdown@1.0.3 69 | matb33:collection-hooks@0.7.9 70 | matteodem:easy-search@1.4.7 71 | meteor@1.1.4 72 | meteor-platform@1.2.1 73 | minifiers@1.1.3 74 | minimongo@1.0.6 75 | mobile-status-bar@1.0.2 76 | mongo@1.0.11 77 | mongo-livedata@1.0.7 78 | mquandalle:jquery-textcomplete@0.3.6_1 79 | mquandalle:moment@1.0.0 80 | mquandalle:stylus@1.0.9 81 | npm-bcrypt@0.7.7 82 | observe-sequence@1.0.4 83 | ongoworks:speakingurl@1.0.5 84 | ordered-dict@1.0.2 85 | raix:eventemitter@0.1.1 86 | random@1.0.2 87 | reactive-dict@1.0.5 88 | reactive-var@1.0.4 89 | reload@1.1.2 90 | retry@1.0.2 91 | reywood:publish-composite@1.3.5 92 | routepolicy@1.0.3 93 | seriousm:emoji-continued@1.4.0 94 | service-configuration@1.0.3 95 | session@1.0.5 96 | sha@1.0.2 97 | spacebars@1.0.4 98 | spacebars-compiler@1.0.4 99 | srp@1.0.2 100 | stylus@1.0.6 101 | tap:i18n@1.3.2 102 | templating@1.0.10 103 | tracker@1.0.4 104 | ui@1.0.5 105 | underscore@1.0.2 106 | url@1.0.3 107 | webapp@1.1.5 108 | webapp-hashing@1.0.2 109 | -------------------------------------------------------------------------------- /client/styles/datepicker.import.styl: -------------------------------------------------------------------------------- 1 | // Currently not used 2 | 3 | .datepicker-select-date, 4 | .datepicker-select-time { 5 | box-sizing: border-box; 6 | float: left; 7 | width: 50%; 8 | 9 | select { 10 | display: none; 11 | width: 100%; 12 | 13 | &.show { 14 | display: block; 15 | } 16 | } 17 | } 18 | 19 | .datepicker-select-date { 20 | padding-right: 12px; 21 | } 22 | 23 | .datepicker-select-label { 24 | margin-bottom: 4px; 25 | } 26 | 27 | .datepicker-select-input { 28 | padding: 4px 6px; 29 | width: 100%; 30 | } 31 | 32 | .datepicker-confirm-btns .remove-date { 33 | float: right; 34 | margin-right: 0; 35 | } 36 | 37 | .ui-datepicker-header { 38 | position: relative; 39 | min-height: 32px; 40 | } 41 | 42 | .ui-datepicker-header a { 43 | border-radius: 3px; 44 | cursor: pointer; 45 | display: block; 46 | padding: 6px 10px; 47 | position: absolute; 48 | left: 0; 49 | top: 0; 50 | } 51 | 52 | .ui-datepicker-header { 53 | 54 | a:hover { 55 | background-color: #dbdbdb; 56 | } 57 | 58 | a.ui-datepicker-prev, 59 | a.ui-datepicker-next { 60 | color: #8c8c8c; 61 | text-decoration: underline; 62 | 63 | &:hover { 64 | color: #4d4d4d; 65 | text-decoration: underline; 66 | } 67 | } 68 | 69 | a.ui-datepicker-next { 70 | left: auto; 71 | right: 0; 72 | text-align: right; 73 | } 74 | 75 | .ui-datepicker-title { 76 | padding: 6px 0; 77 | margin-bottom: 4px; 78 | text-align: center; 79 | } 80 | } 81 | 82 | .ui-datepicker-calendar { 83 | 84 | td, 85 | th { 86 | position: relative; 87 | text-align: center; 88 | padding: 0; 89 | width: 14%; 90 | 91 | a { 92 | display: block; 93 | padding: 5px; 94 | text-decoration: none; 95 | 96 | &:hover { 97 | background-color: #e3e3e3; 98 | color: #4d4d4d; 99 | } 100 | } 101 | } 102 | 103 | th { 104 | padding: 5px; 105 | } 106 | 107 | .ui-priority-secondary { 108 | background-color: #f0f0f0; 109 | color: #8c8c8c; 110 | 111 | &:hover { 112 | background-color: #dbdbdb; 113 | color: #4d4d4d; 114 | } 115 | } 116 | 117 | .ui-state-highlight { 118 | background: #e3e3e3; 119 | color: #4d4d4d; 120 | 121 | &:hover { 122 | background-color: #dbdbdb; 123 | color: #4d4d4d; 124 | } 125 | } 126 | 127 | .ui-priority-secondary.ui-state-active:hover, 128 | .ui-state-active, 129 | .ui-state-active:hover { 130 | background: #2e85b8; 131 | color: #fff; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /client/lib/filter.js: -------------------------------------------------------------------------------- 1 | // Filtered view manager 2 | // We define local filter objects for each different type of field (SetFilter, 3 | // RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose 4 | // goal is to filter complete documents by using the local filters for each 5 | // fields. 6 | 7 | 8 | // Use a "set" filter for a field that is a set of documents uniquely 9 | // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`. 10 | var SetFilter = function(name) { 11 | this._dep = new Tracker.Dependency; 12 | this._selectedElements = []; 13 | }; 14 | 15 | _.extend(SetFilter.prototype, { 16 | isSelected: function(val) { 17 | this._dep.depend(); 18 | return this._selectedElements.indexOf(val) > -1; 19 | }, 20 | // XXX We don't have a `add` and `remove` methods here, that would probably 21 | // makes sense, and would be trivial to write but since there is currently 22 | // no need in the code, I didn't write these. 23 | toogle: function(val) { 24 | var indexOfVal = this._selectedElements.indexOf(val); 25 | if (indexOfVal === -1) { 26 | this._selectedElements.push(val); 27 | } else { 28 | this._selectedElements.splice(indexOfVal, 1); 29 | } 30 | this._dep.changed(); 31 | }, 32 | reset: function() { 33 | this._selectedElements = []; 34 | this._dep.changed(); 35 | }, 36 | _isActive: function() { 37 | this._dep.depend(); 38 | return this._selectedElements.length !== 0; 39 | }, 40 | _getMongoSelector: function() { 41 | this._dep.depend(); 42 | return { $in: this._selectedElements }; 43 | } 44 | }); 45 | 46 | // The global Filter object. 47 | // XXX It would be possible to re-write this object more elegantly, and removing 48 | // the need to provide a list of `_fields`. We also should move methods into the 49 | // object prototype. 50 | Filter = { 51 | _fields: ['labelIds', 'members'], 52 | 53 | // XXX I would like to rename this field into `labels` to be consistent with 54 | // the rest of the schema, but we need to set some migrations architecture 55 | // before changing the schema. 56 | labelIds: new SetFilter(), 57 | members: new SetFilter(), 58 | 59 | isActive: function() { 60 | var self = this; 61 | return _.any(self._fields, function(fieldName) { 62 | return self[fieldName]._isActive(); 63 | }); 64 | }, 65 | 66 | getMongoSelector: function() { 67 | var self = this; 68 | var mongoSelector = {}; 69 | _.forEach(self._fields , function(fieldName) { 70 | var filter = self[fieldName]; 71 | if (filter._isActive()) 72 | mongoSelector[fieldName] = filter._getMongoSelector(); 73 | }); 74 | return mongoSelector; 75 | }, 76 | 77 | reset: function() { 78 | var self = this; 79 | _.forEach(self._fields , function(fieldName) { 80 | var filter = self[fieldName]; 81 | filter.reset(); 82 | }); 83 | } 84 | }; 85 | 86 | Blaze.registerHelper('Filter', Filter); 87 | -------------------------------------------------------------------------------- /client/views/boards/header.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | 3 | .board-header { 4 | height: auto; 5 | overflow: hidden; 6 | padding: 10px 30px 10px 8px; 7 | position: relative; 8 | transition: padding .15s ease-in; 9 | } 10 | 11 | .board-header-btns { 12 | position: relative; 13 | display: block; 14 | } 15 | 16 | .board-header-btn { 17 | border-radius: 3px; 18 | color: #f6f6f6; 19 | cursor: default; 20 | float: left; 21 | font-size: 12px; 22 | height: 30px; 23 | line-height: 32px; 24 | margin: 0 4px 0 0; 25 | overflow: hidden; 26 | padding-left: 30px; 27 | position: relative; 28 | text-decoration: none; 29 | } 30 | 31 | .board-header-btn:empty { 32 | display: none; 33 | } 34 | 35 | .board-header-btn-without-icon { 36 | padding-left: 8px; 37 | } 38 | 39 | .board-header-btn-icon { 40 | background-clip: content-box; 41 | background-origin: content-box; 42 | color: #f6f6f6 !important; 43 | padding: 6px; 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | } 48 | 49 | .board-header-btn-text { 50 | padding-right: 8px; 51 | } 52 | 53 | .board-header-btn:not(.no-edit) .text { 54 | text-decoration: underline; 55 | } 56 | 57 | .board-header-btn:not(.no-edit):hover { 58 | background: rgba(0, 0, 0, .12); 59 | cursor: pointer; 60 | } 61 | 62 | .board-header-btn:hover { 63 | color: #f6f6f6; 64 | } 65 | 66 | .board-header-btn.board-header-btn-enabled { 67 | background-color: rgba(0, 0, 0, .1); 68 | 69 | &:hover { 70 | background-color: rgba(0, 0, 0, .3); 71 | } 72 | 73 | .board-header-btn-icon.icon-star { 74 | color: #e6bf00 !important; 75 | } 76 | } 77 | 78 | .board-header-btn-name { 79 | cursor: default; 80 | font-size: 18px; 81 | font-weight: 700; 82 | line-height: 30px; 83 | padding-left: 4px; 84 | text-decoration: none; 85 | 86 | .board-header-btn-text { 87 | padding-left: 6px; 88 | } 89 | } 90 | 91 | .board-header-btn-name-org-logo { 92 | border-radius: 3px; 93 | height: 30px; 94 | left: 0; 95 | position: absolute; 96 | top: 0; 97 | width: 30px; 98 | 99 | .board-header-btn-text { 100 | padding-left: 32px; 101 | } 102 | } 103 | 104 | .board-header-btn-org-name { 105 | overflow: hidden; 106 | text-overflow: ellipsis; 107 | white-space: nowrap; 108 | max-width: 400px; 109 | } 110 | 111 | .board-header-btn-filter-indicator { 112 | background: #3d990f; 113 | padding-right: 30px; 114 | color: #fff; 115 | text-shadow: 0; 116 | 117 | &:hover { 118 | background: #43a711 !important; 119 | } 120 | 121 | .board-header-btn-icon-close { 122 | background: #43a711; 123 | border-top-left-radius: 0; 124 | border-top-right-radius: 3px; 125 | border-bottom-right-radius: 3px; 126 | border-bottom-left-radius: 0; 127 | color: #fff; 128 | padding: 6px; 129 | position: absolute; 130 | right: 0; 131 | top: 0; 132 | 133 | &:hover { 134 | background: #48b512; 135 | } 136 | } 137 | } 138 | 139 | .extra-large-window, 140 | .large-window, 141 | .medium-window { 142 | 143 | .board-wrapper.disabled-all-widgets .board-header { 144 | padding-right: 136px; 145 | } 146 | 147 | .board-header { 148 | padding-right: 296px; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /client/views/users/events.js: -------------------------------------------------------------------------------- 1 | Template.login.events({ 2 | 'submit #LoginForm': function(event, t) { 3 | var email = $.trim(t.find('#email').value), 4 | password = $.trim(t.find('#password').value); 5 | 6 | if (email && password) { 7 | Meteor.loginWithPassword(email, password, function(err) { 8 | 9 | // show error and return false; 10 | if (err) { Utils.error(err); return; } 11 | 12 | // Redirect to Boards page 13 | Router.go('Boards'); 14 | }); 15 | } 16 | 17 | // submit false. 18 | event.preventDefault(); 19 | } 20 | }); 21 | 22 | Template.signup.events({ 23 | 'submit #SignUpForm': function(event, t) { 24 | var email = $.trim(t.find('#email').value), 25 | name = $.trim(t.find('#name').value), 26 | password = $.trim(t.find('#password').value), 27 | options = { 28 | email: email, 29 | password: password, 30 | profile: { 31 | name: name, 32 | language: TAPi18n.getLanguage() 33 | } 34 | }; 35 | 36 | if (email && name && password) { 37 | Accounts.createUser(options, function(err) { 38 | 39 | // show error and return false; 40 | if (err) { Utils.error(err); return; } 41 | 42 | // Redirect to Boards page 43 | Router.go('Boards'); 44 | }); 45 | } 46 | event.preventDefault(); 47 | } 48 | }); 49 | 50 | 51 | Template.memberHeader.events({ 52 | 'click .js-open-header-member-menu': Popup.open('memberMenu'), 53 | 'click .js-open-add-menu': Popup.open('createBoard') 54 | }); 55 | 56 | Template.memberMenuPopup.events({ 57 | 'click .js-language': Popup.open('setLanguage'), 58 | 'click .js-logout': function(event, t) { 59 | event.preventDefault(); 60 | 61 | Meteor.logout(function() { 62 | Router.go('Home'); 63 | }); 64 | } 65 | }); 66 | 67 | Template.setLanguagePopup.events({ 68 | 'click .js-set-language': function(event) { 69 | Users.update(Meteor.userId(), { 70 | $set: { 71 | 'profile.language': this.tag 72 | } 73 | }); 74 | event.preventDefault(); 75 | } 76 | }); 77 | 78 | Template.profileEditForm.events({ 79 | 'click .js-edit-profile': function() { 80 | Session.set('ProfileEditForm', true); 81 | }, 82 | 'click .js-cancel-edit-profile': function() { 83 | Session.set('ProfileEditForm', false); 84 | }, 85 | 'submit #ProfileEditForm': function(event, t) { 86 | var name = t.find('#name').value, 87 | bio = t.find('#bio').value; 88 | 89 | // trim and update 90 | if ($.trim(name)) { 91 | Users.update(this.profile()._id, { 92 | $set: { 93 | 'profile.name': name, 94 | 'profile.bio': bio 95 | } 96 | }, function() { 97 | 98 | // update complete close profileEditForm 99 | Session.set('ProfileEditForm', false); 100 | }); 101 | } 102 | event.preventDefault(); 103 | } 104 | }); 105 | 106 | 107 | Template.memberName.events({ 108 | 'click .js-show-mem-menu': Popup.open('user') 109 | }); 110 | -------------------------------------------------------------------------------- /client/views/lists/templates.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 32 | 33 | 47 | 48 | 65 | 66 | 79 | 80 | 84 | -------------------------------------------------------------------------------- /client/views/widgets/events.js: -------------------------------------------------------------------------------- 1 | Template.boardWidgets.events({ 2 | 'click .js-show-sidebar': function(event, t) { 3 | Session.set('sidebarIsOpen', true); 4 | }, 5 | 'click .js-hide-sidebar': function() { 6 | Session.set('sidebarIsOpen', false); 7 | }, 8 | 'click .js-pop-widget-view': function() { 9 | Session.set('currentWidget', 'home'); 10 | } 11 | }); 12 | 13 | Template.menuWidget.events({ 14 | 'click .js-open-card-filter': function() { 15 | Session.set('currentWidget', 'filter'); 16 | }, 17 | 'click .js-change-background': function() { 18 | Session.set('currentWidget', 'background'); 19 | }, 20 | 'click .js-close-board': Popup.afterConfirm('closeBoard', function() { 21 | Boards.update(this.board._id, { 22 | $set: { 23 | archived: true 24 | } 25 | }); 26 | 27 | Router.go('Boards'); 28 | }), 29 | 'click .js-toggle-widget-nav': function(event, t) { 30 | Session.set('menuWidgetIsOpen', ! Session.get('menuWidgetIsOpen')); 31 | } 32 | }); 33 | 34 | Template.filterWidget.events({ 35 | 'click .js-toggle-label-filter': function(event) { 36 | Filter.labelIds.toogle(this._id); 37 | event.preventDefault(); 38 | }, 39 | 'click .js-toogle-member-filter': function(event) { 40 | Filter.members.toogle(this._id); 41 | event.preventDefault(); 42 | } 43 | }); 44 | 45 | Template.backgroundWidget.events({ 46 | 'click .js-select-background': function(event) { 47 | var currentBoardId = Router.current().params.boardId; 48 | Boards.update(currentBoardId, {$set: { 49 | background: { 50 | type: 'color', 51 | color: this.toString() 52 | } 53 | }}); 54 | event.preventDefault(); 55 | } 56 | }); 57 | 58 | var getMemberIndex = function(board, searchId) { 59 | for (var i = 0; i < board.members.length; i++) { 60 | if (board.members[i].userId === searchId) 61 | return i; 62 | } 63 | throw new Meteor.Error("Member not found"); 64 | } 65 | 66 | Template.memberPopup.events({ 67 | 'click .js-change-role': Popup.open('changePermissions'), 68 | 'click .js-remove-member:not(.disabled)': Popup.afterConfirm('removeMember', function(){ 69 | var currentBoardId = Router.current().params.boardId; 70 | Boards.update(currentBoardId, {$pull: {members: {userId: this.userId}}}); 71 | Popup.close(); 72 | }), 73 | 'click .js-leave-member': function(event, t) { 74 | // @TODO 75 | 76 | Popup.close(); 77 | } 78 | }); 79 | 80 | Template.membersWidget.events({ 81 | 'click .js-open-manage-board-members': Popup.open('addMember'), 82 | 'click .member': Popup.open('member') 83 | }); 84 | 85 | Template.addMemberPopup.events({ 86 | 'click .pop-over-member-list li:not(.disabled)': function(event, t) { 87 | var userId = this._id; 88 | var boardId = t.data.board._id; 89 | Boards.update(boardId, { 90 | $push: { 91 | members: { 92 | userId: userId, 93 | isAdmin: false 94 | } 95 | } 96 | }); 97 | Popup.close(); 98 | } 99 | }); 100 | 101 | Template.changePermissionsPopup.events({ 102 | 'click .js-set-admin, click .js-set-normal': function(event, t) { 103 | var currentBoard = Boards.findOne(Router.current().params.boardId); 104 | var memberIndex = getMemberIndex(currentBoard, this.user._id); 105 | var isAdmin = $(event.currentTarget).hasClass('js-set-admin'); 106 | var setQuery = {}; 107 | setQuery[['members', memberIndex, 'isAdmin'].join('.')] = isAdmin; 108 | Boards.update(currentBoard._id, { $set: setQuery }); 109 | Popup.back(1); 110 | } 111 | }); 112 | -------------------------------------------------------------------------------- /client/styles/print.styl: -------------------------------------------------------------------------------- 1 | @media print { 2 | * { 3 | text-shadow: none!important; 4 | } 5 | 6 | #surface { 7 | height: auto!important; 8 | } 9 | 10 | body { 11 | overflow: visible!important; 12 | } 13 | 14 | .attachment-thumbnail, 15 | .phenom, img { 16 | page-break-before: auto; 17 | page-break-after: auto; 18 | page-break-inside: avoid; 19 | position: relative; 20 | } 21 | 22 | #header { 23 | display: none; 24 | } 25 | 26 | #notification { 27 | display: none!important; 28 | } 29 | 30 | #board { 31 | height: auto!important; 32 | margin-left: 8px!important; 33 | overflow: visible!important; 34 | display: block!important; 35 | } 36 | 37 | .list { 38 | display: block; 39 | float: none; 40 | margin-bottom: 16px; 41 | max-height: none!important; 42 | width: 400px!important; 43 | } 44 | 45 | .list-cards { 46 | max-height: none!important; 47 | -moz-box-flex: 0; 48 | -webkit-box-flex: 0; 49 | -ms-flex: 0 0 auto; 50 | -webkit-flex: 0 0 auto; 51 | flex: 0 0 auto; 52 | } 53 | 54 | .extra-large-window .list, .large-window .list { 55 | -webkit-box-shadow: none; 56 | box-shadow: none; 57 | float: none; 58 | } 59 | 60 | .list-header .icon-sm { 61 | visibility: hidden; 62 | } 63 | 64 | .list-header .list-title { 65 | margin-left: 12px; 66 | } 67 | 68 | .open-card-composer { 69 | display: none; 70 | } 71 | 72 | .list-card { 73 | border: 1px solid #dbdbdb; 74 | border-bottom-color: #c2c2c2; 75 | width: 340px; 76 | } 77 | 78 | .badge, .board-widgets, .list-card-votes, .member .status { 79 | display: none; 80 | } 81 | 82 | .pop-over, .other-actions, .quiet-actions, .window-sidebar .button-link, .card-detail-desc .quiet-button, .chechklist-new-item, .new-comment { 83 | display: none!important; 84 | } 85 | 86 | .card-label { 87 | color: #4d4d4d; 88 | } 89 | 90 | .hide-on-edit, .card-detail-item-add-button { 91 | display: none; 92 | } 93 | 94 | .checklist-progress-bar { 95 | -webkit-box-shadow: none; 96 | box-shadow: none; 97 | border: 1px solid #e3e3e3; 98 | } 99 | 100 | .checklist-progress-bar-current { 101 | height: 7px; 102 | margin-top: 1px; 103 | border: 1px solid #777; 104 | } 105 | 106 | .action-comment { 107 | border: 1px solid #dbdbdb; 108 | -webkit-box-shadow: none; 109 | box-shadow: none; 110 | } 111 | 112 | .editable .current { 113 | display: block!important; 114 | } 115 | 116 | .editable .edit, .editable .edits-warning { 117 | display: none!important; 118 | } 119 | 120 | body.window-up #content { 121 | display: none; 122 | } 123 | 124 | body.window-up #surface { 125 | display: none; 126 | min-height: initial; 127 | height: auto!important; 128 | } 129 | 130 | body.window-up .window-overlay { 131 | background: #fff; 132 | display: block!important; 133 | height: auto; 134 | overflow: visible; 135 | position: relative; 136 | } 137 | 138 | body.window-up .window-sidebar, body.window-up .dialog-close-button { 139 | display: none; 140 | } 141 | 142 | body.window-up .window { 143 | position: relative!important; 144 | left: auto!important; 145 | top: auto!important; 146 | width: 700px!important; 147 | } 148 | 149 | body.window-up .window input, body.window-up .window textarea { 150 | display: none; 151 | } 152 | 153 | body.window-up .window-wrapper { 154 | background: #fff; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /client/styles/metrello.css: -------------------------------------------------------------------------------- 1 | /** 2 | * We should merge these declarations in the appropriate stylus files. 3 | */ 4 | 5 | #AddBoardForm input[type='text'] { width:234px; } 6 | .boardPage { background-color: rgb(35, 113, 159); } 7 | .dn { display:none; } 8 | .header-btn-btn { padding-left:23px!important; ; } 9 | .whiteBg { background:white; } 10 | .bgnone { background:none!important; } 11 | .cursorDefault { cursor:default!important; } 12 | .tac { text-align:center; } 13 | .pb100 { padding-bottom:100px!important; } 14 | .mt50 { margin-top:50px; } 15 | .list-header-name { word-wrap: break-word; } 16 | .list-card-details:hover { background:#f2f2f2;border-bottom-color:#b3b3b3; } 17 | .tdn { text-decoration:none; } 18 | 19 | .header-member { min-width:105px!important; text-align:center; } 20 | 21 | .white { color:white!important; } 22 | .primarys { font-size:20px; line-height: 1.44em; padding: .6em 1.3em!important; border-radius: 3px!important; box-shadow: 0 2px 0 #4d4d4d!important; } 23 | .layout-twothirds-center { display: block; max-width: 585px; margin: 0 auto; position: relative; font-size:20px; line-height: 100px; } 24 | 25 | a.list-card-details { text-decoration:none; } 26 | 27 | #WindowTitleEdit .single-line, .single-line2 { overflow: hidden; word-wrap: break-word; resize: none; height: 60px; } 28 | .single-line2 { overflow: hidden; word-wrap: break-word; resize: none; height: 108px; } 29 | .fieldSingle { overflow: hidden; word-wrap: break-word; resize: none; height: 34px; } 30 | 31 | .create_board, .user_header_menu { right:5px!important; top:45px!important; display:block!important; } 32 | .rename_board { left:8px!important; top: 85px!important; display:block!important } 33 | .permission_level { left: 81px!important; top: 85px!important; max-height: 370px!important; display: block!important; } 34 | 35 | .bgBoard { 36 | background-color: #16A085; 37 | } 38 | 39 | .promo-nav a#navLanding {background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAICAMAAAD6Ou7DAAAAM1BMVEUAAAD///////////////////////////////////////////////////////////////+3leKCAAAAEHRSTlMAlg/kKQc7w9X4AX9QUf799Yq2lgAAADZJREFUCB0FwYUBgAAAwCDs1v1/rQDcDwDjNI0AhhoA1q/eFVj2qn0BV1VdMB9VdcycW1XVdv5TywJ7mq6QbgAAAABJRU5ErkJggg==') no-repeat center bottom;cursor:default;color:#a2d6f4;padding-bottom:12px} 40 | 41 | .base-loading { background:url('/img/middle_logo.png'); width:80px; height:30px; margin:50px auto; } 42 | .base-loading .loading-gif { background:url(/img/loading.gif); background-size: 18px 18px; height: 18px; left: 2px; position: relative; top: 6px; width: 18px; } 43 | 44 | .sortable-dragging { cursor:-webkit-grab; } 45 | .list-area > .placeholder { background-color:rgba(0,0,0,0.2)!important; border-color:transparent!important; -webkit-box-shadow:none!important; box-shadow:none!important; float:left; width:250px; margin: 0 5px; padding: 4px 3px 3px;} 46 | 47 | #header-search { float:left; margin:1px 8px 0 0; position:relative; z-index:1; } 48 | #header-search label { display:none; } 49 | #header-search input[type="text"] { background:rgba(255,255,255,0.5);border-top-left-radius:3px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px;border:none;float:left;font-size:13px;height:29px;min-height:29px;line-height:19px;width:160px;margin:0} 50 | #header-search input[type="text"]:hover{background:rgba(255,255,255,0.7)} 51 | #header-search input[type="text"]:focus{background:#e8ebee;-webkit-box-shadow:none;box-shadow:none} 52 | #header-search .header-btn{border-top-left-radius:0;border-top-right-radius:3px;border-bottom-right-radius:3px;border-bottom-left-radius:0} 53 | #header-search input[type="submit"]{display:none} 54 | 55 | #landingLogo { 56 | clear:both; 57 | text-align:center; 58 | padding: 40px 40px 20px 40px; 59 | font-weight: normal; 60 | } 61 | 62 | .account-header #landingLogo { 63 | font-size: 35px; 64 | padding: 12px; 65 | } 66 | 67 | .width-wrapper {max-width:1080px;margin:0 auto;padding:0 60px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box; } 68 | 69 | .section-wrapper p { font-weight: 300; line-height: 1.44em; padding-bottom:30px; } 70 | .outline-link { border: 1px solid; border-radius: 3px; display: inline-block; margin: .6em 0; padding: .6em 1em; text-decoration: none; color: #5e98ba; background: transparent;} 71 | .tac h1 { font-size: 2em; } 72 | 73 | #Team { margin: 20px auto; max-width: 890px; position: relative; font-family: "jaf-facitweb","Helvetica Neue",Arial,sans-serif; color:#4d4d4d; } 74 | #Team .developer { display: inline-block; vertical-align: top; width: 250px; } 75 | #Team .developer img { width:150px; border:1px solid #c7c7c7; border-radius:4px; } 76 | #Team h4 { font-size:20px; } 77 | #Team i { font-size:25px; margin-right:5px; } 78 | -------------------------------------------------------------------------------- /client/views/lists/events.js: -------------------------------------------------------------------------------- 1 | Template.lists.events({ 2 | 'click .js-open-card-composer': function(event, t) { 3 | var $el = $(event.currentTarget), 4 | list = $el.parents('.list'), 5 | composer = list.find('.card-composer'); 6 | allComposers = t.$('.card-composer'); 7 | 8 | // all lists hide composer and open click composer show 9 | allComposers.addClass('hide'); 10 | t.$('.js-open-card-composer').removeClass('hide'); 11 | 12 | // click open composer and focus 13 | composer.removeClass('hide'); 14 | composer.find('.js-card-title').focus(); 15 | $el.addClass('hide'); 16 | 17 | // stop click hash 18 | event.preventDefault(); 19 | }, 20 | 'click .js-open-list-menu': Popup.open('listAction'), 21 | 'click .hide-on-edit': function(event, t) { 22 | var $this = $(event.currentTarget), 23 | listHeader = $this.parents('.list-header'); 24 | 25 | // remove all editing classes 26 | $('.js-list-header').removeClass('editing'); 27 | 28 | // and current editing open. 29 | listHeader.addClass('editing'); 30 | 31 | // focus current form title 32 | listHeader.find('.single-line').focus(); 33 | 34 | // stop click hash 35 | event.preventDefault(); 36 | } 37 | }); 38 | 39 | Template.listTitleEditForm.events({ 40 | 'submit #ListTitleEditForm': function(event, t) { 41 | var title = t.find('.js-title-input').value; 42 | if ($.trim(title)) { 43 | Lists.update(this.list._id, { 44 | $set: { 45 | title: title 46 | } 47 | }); 48 | 49 | // all editing remove 50 | $('.js-list-header').removeClass('editing'); 51 | } 52 | event.preventDefault(); 53 | }, 54 | 'click .js-cancel-edit': function(event, t) { 55 | $('.js-list-header').removeClass('editing'); 56 | } 57 | }); 58 | 59 | Template.addlistForm.events({ 60 | 'click .js-open-add-list': function(event, t) { 61 | t.$('.list').removeClass('idle'); 62 | t.$('.list-name-input').focus(); 63 | }, 64 | 'click .js-cancel-edit': function(event, t) { 65 | t.$('.list').addClass('idle'); 66 | }, 67 | // XXX We do something similar in the new card form event and should 68 | // probably avoid repeating ourself 69 | // 70 | // Keydown (and not keypress) in necessary here because the `keyCode` 71 | // property is consistent in all browsers, (there is not keyCode for the 72 | // `keypress` event in firefox) 73 | 'keydown .list-name-input': function(event, t) { 74 | var code = event.keyCode; 75 | if (code === 27) { 76 | t.$('.js-cancel-edit').click(); 77 | event.preventDefault(); 78 | } 79 | }, 80 | 'submit #AddListForm': function(event, t) { 81 | var title = t.find('.list-name-input'); 82 | if ($.trim(title.value)) { 83 | // insert 84 | Lists.insert({ 85 | title: title.value, 86 | boardId: this.board._id, 87 | sort: $('.list').length 88 | }, function() { 89 | 90 | // insert complete to scrollLeft 91 | Utils.Scroll('#board').left(270, true); 92 | }); 93 | 94 | // clear input 95 | title.value = ''; 96 | } 97 | event.preventDefault(); 98 | } 99 | }); 100 | 101 | Template.listActionPopup.events({ 102 | 'click .js-add-card': function(event, t) {}, 103 | 'click .js-list-subscribe': function(event, t) {}, 104 | 'click .js-move-cards': Popup.open('listMoveCards'), 105 | 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { 106 | Cards.find({listId: this._id}).forEach(function(card) { 107 | Cards.update(card._id, { 108 | $set: { 109 | archived: true 110 | } 111 | }) 112 | }); 113 | Popup.close(); 114 | }), 115 | 'click .js-close-list': function(event, t) { 116 | Lists.update(this._id, { 117 | $set: { 118 | archived: true 119 | } 120 | }); 121 | 122 | Popup.close(); 123 | 124 | event.preventDefault(); 125 | } 126 | }); 127 | 128 | Template.listMoveCardsPopup.events({ 129 | 'click .js-select-list': function() { 130 | var fromList = Template.parentData(2).data._id; 131 | var toList = this._id; 132 | Cards.find({listId: fromList}).forEach(function(card) { 133 | Cards.update(card._id, { 134 | $set: { 135 | listId: toList 136 | } 137 | }); 138 | }); 139 | Popup.close(); 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /client/lib/utils.js: -------------------------------------------------------------------------------- 1 | Utils = { 2 | error: function(err) { 3 | Session.set("error", (err && err.message || false)); 4 | }, 5 | 6 | is_authenticated: function() { 7 | return Meteor.user() ? true : false; 8 | }, 9 | 10 | resizeHeight: function(selector, callback) { 11 | return function() { 12 | var board = jQuery(selector); 13 | var headerSize = $('#header').outerHeight(); 14 | board.height($(window).height() - 60 - headerSize); 15 | 16 | // call 17 | callback && callback(); 18 | } 19 | }, 20 | 21 | boardScrollLeft: function() { 22 | var el = jQuery('#board'), 23 | data = Blaze.getData(el.get(0)); 24 | if (data) { 25 | var sessionName = data.board ? 'scrollBoardLeft-' + data.board._id : false; 26 | 27 | if (sessionName) { 28 | el.scroll(function() { 29 | Session.set(sessionName, $(this).scrollLeft()); 30 | }); 31 | el.scrollLeft(Session.get(sessionName)); 32 | } 33 | } 34 | }, 35 | 36 | widgetsHeight: function() { 37 | var wrapper = $('.board-wrapper'), 38 | widgets = $('.board-widgets'), 39 | boardActions = $('.board-actions-list'), 40 | pop = $('.pop-over'); 41 | 42 | // set height widgets 43 | widgets.height(wrapper.height()); 44 | boardActions.height(wrapper.height() - 215); 45 | pop.find('.content').css('max-height', widgets.height() / 2); 46 | }, 47 | 48 | // scroll 49 | Scroll: function(selector) { 50 | var $el = $(selector); 51 | return { 52 | top: function(px, add) { 53 | var t = $el.scrollTop(); 54 | $el.animate({ scrollTop: (add ? (t + px) : px) }); 55 | }, 56 | left: function(px, add) { 57 | var l = $el.scrollLeft(); 58 | $el.animate({ scrollLeft: (add ? (l + px) : px) }); 59 | } 60 | }; 61 | }, 62 | 63 | Warning: { 64 | get: function() { 65 | return Session.get('warning'); 66 | }, 67 | open: function(desc) { 68 | Session.set('warning', { desc: desc }); 69 | }, 70 | close: function() { 71 | Session.set('warning', false); 72 | } 73 | }, 74 | 75 | goBoardId: function(_id) { 76 | var board = Boards.findOne(_id); 77 | return board && Router.go('Board', { boardId: board._id, slug: board.slug }); 78 | }, 79 | 80 | liveEvent: function(events, callback) { 81 | $(document).on(events, function() { 82 | callback($(this)); 83 | }); 84 | }, 85 | 86 | // new getCardData(cardEl) 87 | getCardData: function(item) { 88 | var el = item.get(0), 89 | card = Blaze.getData(el), 90 | list = Blaze.getData(item.parents('.list').get(0)), 91 | before = item.prev('.card').get(0), 92 | after = item.next('.card').get(0); 93 | 94 | this.listId = list._id; 95 | this.cardId = card._id; 96 | 97 | // if before and after undefined then sort 0 98 | if (!before && !after) { 99 | 100 | // set sort 0 101 | this.sort = 0; 102 | } else { 103 | 104 | /* 105 | * 106 | * Blaze.getData takes as a parameter an html element 107 | * and will return the data context that was bound when 108 | * that html element was rendered! 109 | */ 110 | if(!before) { 111 | 112 | /* 113 | * if it was dragged into the first position grab the 114 | * next element's data context and subtract one from the rank 115 | */ 116 | this.sort = Blaze.getData(after).sort - 1; 117 | } else if(!after) { 118 | /* 119 | * if it was dragged into the last position grab the 120 | * previous element's data context and add one to the rank 121 | */ 122 | this.sort = Blaze.getData(before).sort + 1; 123 | } else { 124 | 125 | /* 126 | * else take the average of the two ranks of the previous 127 | * and next elements 128 | */ 129 | this.sort = (Blaze.getData(after).sort + Blaze.getData(before).sort) / 2; 130 | } 131 | } 132 | }, 133 | getLabelIndex: function(boardId, labelId) { 134 | var board = Boards.findOne(boardId), 135 | labels = {}; 136 | _.each(board.labels, function(a, b) { 137 | labels[a._id] = b; 138 | }); 139 | return { 140 | index: labels[labelId], 141 | key: function(key) { 142 | return 'labels.' + labels[labelId] + '.' + key; 143 | } 144 | } 145 | } 146 | }; 147 | 148 | InputsCache = new ReactiveDict('inputsCache'); 149 | 150 | Blaze.registerHelper('inputCache', function (formName, formInstance) { 151 | var key = formName.toString() + '-' + formInstance.toString(); 152 | return InputsCache.get(key); 153 | }); 154 | -------------------------------------------------------------------------------- /client/styles/aging.import.styl: -------------------------------------------------------------------------------- 1 | // Currently not used 2 | 3 | .aging-regular.aging-level-0 { 4 | opacity: 1; 5 | } 6 | 7 | .aging-regular.aging-level-1 { 8 | opacity: .75; 9 | } 10 | 11 | .aging-regular.aging-level-2 { 12 | opacity: .5; 13 | } 14 | 15 | .aging-regular.aging-level-3 { 16 | opacity: .25; 17 | } 18 | 19 | .aging-pirate { 20 | background-position: top left, top right; 21 | background-repeat: no-repeat; 22 | } 23 | 24 | .aging-pirate .list-card-details { 25 | background-position: bottom left, bottom right; 26 | background-repeat: no-repeat; 27 | } 28 | 29 | .aging-pirate.aging-level-1 { 30 | background-color: #faf6ef; 31 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/53102e7b253303f6eb34f3622da9bb32/TopLeftLevel1.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/b6830a5537c94f982a32b280cff6b97d/TopRightLevel1.png); 32 | -webkit-box-shadow: inset 0 0 15px rgba(205, 172, 132, .4); 33 | box-shadow: inset 0 0 15px rgba(205, 172, 132, .4); 34 | } 35 | 36 | .aging-pirate.aging-level-1 .list-card-operation { 37 | background-color: #faf6ef; 38 | } 39 | 40 | .aging-pirate.aging-level-1 .list-card-cover, .aging-pirate.aging-level-1 .list-card-stickers-area .stickers { 41 | opacity: .75; 42 | } 43 | 44 | .aging-pirate.aging-level-1 .list-card-details { 45 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/accbe663e1d810cfb14d7e978845e27f/BottomLeftLevel1.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/2fdf1b0eadc4b04edaece4c4b41aa699/BottomRightLevel1.png); 46 | } 47 | 48 | .aging-pirate.aging-level-1.has-stickers .list-card-details { 49 | background-color: rgba(250, 246, 239, .7); 50 | } 51 | 52 | .aging-pirate.aging-level-1.active-card, .aging-pirate.aging-level-1.active-card .list-card-operation { 53 | background-color: #f4ead7; 54 | } 55 | 56 | .aging-pirate.aging-level-1.active-card.has-stickers .list-card-details { 57 | background-color: rgba(244, 234, 215, .7); 58 | } 59 | 60 | .aging-pirate.aging-level-2 { 61 | background-color: #f6eedf; 62 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/18f4120762d23b79c07e5fda7a2056d0/TopLeftLevel2.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/c74a8a1f361b08433f2a8b05470917c3/TopRightLevel2.png); 63 | -webkit-box-shadow: inset 0 0 25px rgba(205, 172, 132, .5); 64 | box-shadow: inset 0 0 25px rgba(205, 172, 132, .5); 65 | } 66 | 67 | .aging-pirate.aging-level-2 .list-card-operation { 68 | background-color: #f5ecdb; 69 | } 70 | 71 | .aging-pirate.aging-level-2 .list-card-cover, .aging-pirate.aging-level-2 .list-card-stickers-area .stickers { 72 | opacity: .5; 73 | } 74 | 75 | .aging-pirate.aging-level-2 .list-card-details { 76 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/602baf1ea202cc5f5c34a30bd5e364f4/BottomLeftLevel2.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/ac74c3df4a788a4eeb59d8a0f6d0f235/BottomRightLevel2.png); 77 | } 78 | 79 | .aging-pirate.aging-level-2.has-stickers .list-card-details { 80 | background-color: rgba(246, 238, 223, .7); 81 | } 82 | 83 | .aging-pirate.aging-level-2.active-card, .aging-pirate.aging-level-2.active-card .list-card-operation { 84 | background-color: #efe1c8; 85 | } 86 | 87 | .aging-pirate.aging-level-2.active-card.has-stickers .list-card-details { 88 | background-color: rgba(239, 225, 200, .7); 89 | } 90 | 91 | .aging-pirate.aging-level-3 { 92 | background-color: #efe1c8; 93 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/194454130e88f74111874a8935ecf3ba/TopLeftLevel3.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/8c21cb29b9a909eecc47678db2ef1654/TopRightLevel3.png); 94 | -webkit-box-shadow: inset 0 0 40px rgba(205, 172, 132, .6); 95 | box-shadow: inset 0 0 40px rgba(205, 172, 132, .6); 96 | } 97 | 98 | .aging-pirate.aging-level-3 .list-card-operation { 99 | background-color: #efe1c8; 100 | } 101 | 102 | .aging-pirate.aging-level-3 .list-card-cover, .aging-pirate.aging-level-3 .list-card-stickers-area .stickers { 103 | opacity: .25; 104 | } 105 | 106 | .aging-pirate.aging-level-3 .list-card-details { 107 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/d4bb1b3ae5fd2aef9beaf2600113073f/BottomLeftLevel3.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/5737b3080b474c0d96439d6852e31ec3/BottomRightLevel3.png); 108 | } 109 | 110 | .aging-pirate.aging-level-3.has-stickers .list-card-details { 111 | background-color: rgba(239, 225, 200, .7); 112 | } 113 | 114 | .aging-pirate.aging-level-3.active-card, .aging-pirate.aging-level-3.active-card .list-card-operation { 115 | background-color: #e8d4b0; 116 | } 117 | 118 | .aging-pirate.aging-level-3.active-card.has-stickers .list-card-details { 119 | background-color: rgba(232, 212, 176, .7); 120 | } 121 | 122 | .aging-pirate.aging-level-3.aging-treasure .list-card-details { 123 | background-position: bottom left, bottom right, bottom 1px right 10px; 124 | background-image: url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/d4bb1b3ae5fd2aef9beaf2600113073f/BottomLeftLevel3.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/5737b3080b474c0d96439d6852e31ec3/BottomRightLevel3.png), url(https://d78fikflryjgj.cloudfront.net/images/powerups/card-aging/85890fb25d1b8909c095b39344a5e804/BottomRightLevel3Treasure.png); 125 | } 126 | -------------------------------------------------------------------------------- /server/publications/boards.js: -------------------------------------------------------------------------------- 1 | // This is the publication used to display the board list. We publish all the 2 | // non-archived boards: 3 | // 1. that the user is a member of 4 | // 2. the user has starred 5 | Meteor.publish('boards', function() { 6 | // Ensure that the user is connected 7 | check(this.userId, String); 8 | 9 | // Defensive programming to verify that starredBoards has the expected 10 | // format -- since the field is in the `profile` a user can modify it. 11 | var starredBoards = Users.findOne(this.userId).profile.starredBoards || []; 12 | check(starredBoards, [String]); 13 | 14 | return Boards.find({ 15 | archived: false, 16 | $or: [ 17 | { 'members.userId': this.userId }, 18 | { _id: { $in: starredBoards } } 19 | ] 20 | }, { 21 | fields: { 22 | _id: 1, 23 | slug: 1, 24 | title: 1, 25 | background: 1 26 | } 27 | }); 28 | }); 29 | 30 | Meteor.publishComposite('board', function(boardId, slug) { 31 | check(boardId, String); 32 | check(slug, String); 33 | return { 34 | find: function() { 35 | return Boards.find({ 36 | _id: boardId, 37 | slug: slug, 38 | archived: false, 39 | $or: [ 40 | // If the board is not public the user has to be a member of 41 | // it to see it. 42 | { permission: 'public' }, 43 | { 'members.userId': this.userId } 44 | ] 45 | }, { limit: 1 }); 46 | }, 47 | // XXX For efficiency we shouldn't publish all activities and comments 48 | // in this publication, and instead use the card publication for that 49 | // purpose. 50 | children: [ 51 | // Lists 52 | { 53 | find: function(board) { 54 | return Lists.find({ 55 | boardId: board._id 56 | }); 57 | } 58 | }, 59 | 60 | // Cards and cards comments 61 | // XXX Originally we were publishing the card documents as a child 62 | // of the list publication defined above using the following 63 | // selector `{ listId: list._id }`. But it was causing a race 64 | // condition in publish-composite, that I documented here: 65 | // 66 | // https://github.com/englue/meteor-publish-composite/issues/29 67 | // 68 | // I then tried to replace publish-composite by cottz:publish, but 69 | // it had a similar problem: 70 | // 71 | // https://github.com/Goluis/cottz-publish/issues/4 72 | // https://github.com/libreboard/libreboard/pull/78 73 | // 74 | // The current state of relational publishing in meteor is a bit 75 | // sad, there are a lot of various packages, with various APIs, some 76 | // of them are unmaintained. Fortunately this is something that will 77 | // be fixed by meteor-core at some point: 78 | // 79 | // https://trello.com/c/BGvIwkEa/48-easy-joins-in-subscriptions 80 | // 81 | // And in the meantime our code below works pretty well -- it's not 82 | // even a hack! 83 | { 84 | find: function(board) { 85 | return Cards.find({ 86 | boardId: board._id 87 | }); 88 | }, 89 | 90 | children: [ 91 | // comments 92 | { 93 | find: function(card) { 94 | return CardComments.find({ 95 | cardId: card._id 96 | }); 97 | } 98 | }, 99 | // Attachments 100 | { 101 | find: function(card) { 102 | return Attachments.find({ 103 | cardId: card._id 104 | }); 105 | } 106 | } 107 | ] 108 | }, 109 | 110 | // Board members 111 | { 112 | find: function(board) { 113 | return Users.find({ 114 | _id: { $in: _.pluck(board.members, 'userId') } 115 | }); 116 | } 117 | }, 118 | 119 | // Activities 120 | { 121 | find: function(board) { 122 | return Activities.find({ 123 | boardId: board._id 124 | }); 125 | }, 126 | children: [ 127 | // Activities members. In general activity members are 128 | // already published in the board member publication above, 129 | // but it can be the case that a board member was removed 130 | // and we still want to read his activity history. 131 | // XXX A more efficient way to do this would be to keep a 132 | // {active: Boolean} field in the `board.members` so we can 133 | // publish former board members in one single publication, 134 | // and have a easy way to distinguish between current and 135 | // former members. 136 | { 137 | find: function(activity) { 138 | if (activity.memberId) 139 | return Users.find(activity.memberId); 140 | } 141 | } 142 | ] 143 | } 144 | ] 145 | } 146 | }); 147 | -------------------------------------------------------------------------------- /client/styles/icons.styl: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Trellicons Regular; 3 | src: url(/fonts/trellicons-regular.woff) format("woff"), 4 | url(/fonts/trellicons-regular.ttf) format("truetype"), 5 | url(/fonts/trellicons-regular.svg#trelliconsregular) format("svg"); 6 | font-weight: 400; 7 | font-style: normal 8 | } 9 | 10 | .icon-lg, 11 | .icon-sm { 12 | color: #a6a6a6; 13 | display: inline-block; 14 | font-family: Trellicons Regular; 15 | -webkit-font-smoothing: antialiased; 16 | font-style: normal; 17 | font-weight: 400; 18 | line-height: 1; 19 | text-align: center; 20 | text-decoration: none 21 | 22 | &.custom-image { 23 | text-indent: 100%; 24 | overflow: hidden 25 | 26 | img { 27 | overflow: hidden; 28 | position: absolute; 29 | top: 0; 30 | left: 0 31 | } 32 | } 33 | } 34 | 35 | .icon-lg.dark, 36 | .icon-sm.dark, 37 | .icon-lg.dark-hover:hover, 38 | .icon-sm.dark-hover:hover, 39 | .dark-hover:hover .icon-lg, 40 | .dark-hover:hover .icon-sm { 41 | color: #686868; 42 | text-decoration: none 43 | } 44 | 45 | .icon-lg.light, 46 | .icon-sm.light, 47 | .icon-lg.light-hover:hover, 48 | .icon-sm.light-hover:hover, 49 | .light-hover:hover .icon-lg, 50 | .light-hover:hover .icon-sm { 51 | color: #fff; 52 | text-decoration: none 53 | } 54 | 55 | .icon-sm { 56 | height: 18px; 57 | font-size: 12px; 58 | line-height: 18px; 59 | width: 18px 60 | } 61 | 62 | .icon-lg { 63 | height: 30px; 64 | font-size: 20px; 65 | line-height: 30px; 66 | width: 30px 67 | } 68 | 69 | .icon-org:before, 70 | .icon-org-private:before { 71 | content: "\f000" 72 | } 73 | 74 | .icon-member:before { 75 | content: "\f001" 76 | } 77 | 78 | .icon-board:before { 79 | content: "\f002" 80 | } 81 | 82 | .icon-list:before { 83 | content: "\f003" 84 | } 85 | 86 | .icon-card:before { 87 | content: "\f004" 88 | } 89 | 90 | .icon-notification:before { 91 | content: "\f005" 92 | } 93 | 94 | .icon-trophy:before { 95 | content: "\f006" 96 | } 97 | 98 | .icon-rocket:before { 99 | content: "\f007" 100 | } 101 | 102 | .icon-cal:before { 103 | content: "\f039" 104 | } 105 | 106 | .icon-briefcase:before { 107 | content: "\f037" 108 | } 109 | 110 | .icon-briefcase-bc-color { 111 | color: #71838e 112 | } 113 | 114 | .icon-crown:before { 115 | content: "\f041" 116 | } 117 | 118 | .icon-crown.icon-color { 119 | color: #a89d24 120 | } 121 | 122 | .icon-comment:before { 123 | content: "\f008" 124 | } 125 | 126 | .icon-label:before { 127 | content: "\f009" 128 | } 129 | 130 | .icon-attachment:before { 131 | content: "\f010" 132 | } 133 | 134 | .icon-checklist:before { 135 | content: "\f011" 136 | } 137 | 138 | .icon-vote:before { 139 | content: "\f012" 140 | } 141 | 142 | .icon-clock:before { 143 | content: "\f013" 144 | } 145 | 146 | .icon-archive:before { 147 | content: "\f014" 148 | } 149 | 150 | .icon-activity:before { 151 | content: "\f015" 152 | } 153 | 154 | .icon-follow:before { 155 | content: "\f016" 156 | } 157 | 158 | .icon-kiwi:before { 159 | content: "\f017" 160 | } 161 | 162 | .icon-sticker:before { 163 | content: "\f040" 164 | } 165 | 166 | .icon-cover:before { 167 | content: "\f044" 168 | } 169 | 170 | .icon-gear:before { 171 | content: "\f018" 172 | } 173 | 174 | .icon-info:before { 175 | content: "\f019" 176 | } 177 | 178 | .icon-search:before { 179 | content: "\f020" 180 | } 181 | 182 | .icon-pin:before { 183 | content: "\f021" 184 | } 185 | 186 | .icon-camera:before { 187 | content: "\f038" 188 | } 189 | 190 | .icon-menu:before { 191 | content: "\f022" 192 | } 193 | 194 | .icon-close:before { 195 | content: "\f023" 196 | } 197 | 198 | .icon-leftarrow:before { 199 | content: "\f024" 200 | } 201 | 202 | .icon-rightarrow:before { 203 | content: "\f025" 204 | } 205 | 206 | .icon-add:before { 207 | content: "\f026" 208 | } 209 | 210 | .icon-remove:before { 211 | content: "\f027" 212 | } 213 | 214 | .icon-check:before { 215 | content: "\f028" 216 | } 217 | 218 | .icon-reopen:before { 219 | content: "\f029" 220 | } 221 | 222 | .icon-open:before { 223 | content: "\f045" 224 | } 225 | 226 | .icon-movedown:before,.icon-move:before { 227 | content: "\f030" 228 | } 229 | 230 | .icon-movedown { 231 | -webkit-transform: rotate(90deg); 232 | transform: rotate(90deg) 233 | } 234 | 235 | .icon-edit:before { 236 | content: "\f031" 237 | } 238 | 239 | .icon-desc:before { 240 | content: "\f046" 241 | } 242 | 243 | .icon-star:before { 244 | content: "\f042" 245 | } 246 | 247 | .icon-star-active { 248 | color: #e6bf00 249 | } 250 | 251 | .icon-filter:before { 252 | content: "\f043" 253 | } 254 | 255 | .icon-private:before { 256 | content: "\f032" 257 | } 258 | 259 | .icon-private { 260 | color: #990f0f 261 | } 262 | 263 | .dark-hover:hover .icon-private, 264 | .icon-private.dark-hover:hover { 265 | color: #7d0c0c 266 | } 267 | 268 | .icon-org-private { 269 | color: #e6bf00 270 | } 271 | 272 | .dark-hover:hover .icon-org-private, 273 | .icon-org-private.dark-hover:hover { 274 | color: #c7a600 275 | } 276 | 277 | .icon-public:before { 278 | content: "\f033" 279 | } 280 | 281 | .icon-public { 282 | color: #3d990f 283 | } 284 | 285 | .dark-hover:hover .icon-public, 286 | .icon-public.dark-hover:hover { 287 | color: #327d0c 288 | } 289 | 290 | .icon-facebook:before { 291 | content: "\f034" 292 | } 293 | 294 | .icon-facebook-brand-color { 295 | color: #3b5998 296 | } 297 | 298 | .icon-google:before { 299 | content: "\f035" 300 | } 301 | 302 | .icon-google-brand-color { 303 | color: #e11a31 304 | } 305 | 306 | .icon-twitter:before { 307 | content: "\f036" 308 | } 309 | 310 | .icon-twitter-brand-color { 311 | color: #00aced 312 | } 313 | 314 | .business-class-mark { 315 | background-repeat: no-repeat; 316 | display: block 317 | } 318 | 319 | .emoji { 320 | height: 18px; 321 | width: 18px; 322 | vertical-align: text-bottom 323 | } 324 | -------------------------------------------------------------------------------- /client/views/boards/templates.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | 76 | 77 | 89 | 90 | 97 | 98 | 122 | -------------------------------------------------------------------------------- /client/views/cards/labels.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | 3 | .card-label { 4 | background-color: #b3b3b3; 5 | border-radius: 3px; 6 | color: #fff; 7 | display: block; 8 | font-weight: 700; 9 | margin-right: 3px; 10 | padding: 3px 6px; 11 | position:relative; 12 | text-shadow: 0 0 5px rgba(0, 0, 0, .2), 0 0 2px #000; 13 | overflow: ellipsis; 14 | } 15 | 16 | .card-label-green { 17 | background-color: #3cb500; 18 | } 19 | 20 | .card-label-yellow { 21 | background-color: #fad900; 22 | } 23 | 24 | .card-label-orange { 25 | background-color: #ff9f19; 26 | } 27 | 28 | .card-label-red { 29 | background-color: #eb4646; 30 | } 31 | 32 | .card-label-purple { 33 | background-color: #a632db; 34 | } 35 | 36 | .card-label-blue { 37 | background-color: #0079bf; 38 | } 39 | 40 | .card-label-pink { 41 | background-color: #ff78cb; 42 | } 43 | 44 | .card-label-sky { 45 | background-color: #00c2e0; 46 | } 47 | 48 | .card-label-black { 49 | background-color: #4d4d4d; 50 | } 51 | 52 | .card-label-lime { 53 | background-color: #51e898; 54 | } 55 | 56 | .color-blind-mode-enabled { 57 | 58 | .card-label-lime, 59 | .card-label-purple { 60 | background-image: linear-gradient(to bottom right, 61 | rgba(255, 255, 255, .5) 25%, 62 | transparent 25%, 63 | transparent 50%, 64 | rgba(255, 255, 255, .5) 50%, 65 | rgba(255, 255, 255, .5) 75%, 66 | transparent 75%, 67 | transparent); 68 | background-size: 16px 16px; 69 | } 70 | 71 | .card-label-orange, 72 | .card-label-mocha, 73 | .card-label-black { 74 | background-image: linear-gradient(to right, 75 | rgba(255, 255, 255, .5) 50%, 76 | transparent 50%, 77 | transparent); 78 | background-size: 8px 8px; 79 | } 80 | 81 | .card-label-green, 82 | .card-label-pink { 83 | background-image: linear-gradient(to bottom left, 84 | rgba(255, 255, 255, .5) 25%, 85 | transparent 25%, 86 | transparent 50%, 87 | rgba(255, 255, 255, .5) 50%, 88 | rgba(255, 255, 255, .5) 75%, 89 | transparent 75%, 90 | transparent); 91 | background-size: 16px 16px; 92 | } 93 | 94 | .card-label-yellow, 95 | .card-label-red { 96 | background-image: linear-gradient(135deg, rgba(255, 255, 255, .5) 25%, transparent 25%), 97 | linear-gradient(225deg, rgba(255, 255, 255, .5) 25%, transparent 25%), 98 | linear-gradient(315deg, rgba(255, 255, 255, .5) 25%, transparent 25%), 99 | linear-gradient(45deg, rgba(255, 255, 255, .5) 25%, transparent 25%); 100 | background-size: 12px 12px; 101 | } 102 | } 103 | 104 | .card-label--selectable:hover { 105 | cursor: pointer; 106 | opacity: .8; 107 | } 108 | 109 | .edit-label, 110 | .create-label { 111 | .card-label { 112 | float: left; 113 | height: 30px; 114 | margin: 0 10px 10px 0; 115 | width: 34px; 116 | } 117 | } 118 | 119 | .edit-labels { 120 | 121 | 122 | input[type="text"] { 123 | margin: 4px 0 6px 38px; 124 | width: 243px; 125 | } 126 | 127 | .card-label { 128 | height: 30px; 129 | left: 0; 130 | padding: 1px 5px; 131 | position: absolute; 132 | top: 0; 133 | width: 24px; 134 | } 135 | 136 | .labels-static .card-label { 137 | line-height: 30px; 138 | margin-bottom: 4px; 139 | position: relative; 140 | top: auto; 141 | left: 0; 142 | width: 260px; 143 | } 144 | } 145 | 146 | .board-widgets .edit-labels input[type="text"] { 147 | width: 190px; 148 | } 149 | 150 | .list-card-labels { 151 | position: relative; 152 | z-index: 30; 153 | top: -6px; 154 | 155 | .card-label { 156 | border-radius: 0; 157 | float: left; 158 | height: 4px; 159 | margin-bottom: 1px; 160 | padding: 0; 161 | width: 40px; 162 | line-height: 100px; 163 | } 164 | } 165 | 166 | .card-detail-item-labels .card-label { 167 | border-radius: 3px; 168 | display: block; 169 | float: left; 170 | height: 20px; 171 | line-height: 20px; 172 | margin: 0 4px 4px 0; 173 | min-width: 30px; 174 | padding: 5px 10px; 175 | width: auto; 176 | } 177 | 178 | .editable-labels .card-label:hover { 179 | cursor: pointer; 180 | opacity: .75; 181 | } 182 | 183 | .edit-labels-pop-over { 184 | margin-bottom: 8px; 185 | } 186 | 187 | .edit-labels-pop-over .shortcut { 188 | display: inline-block; 189 | } 190 | 191 | .card-label-selectable { 192 | border-radius: 3px; 193 | cursor: pointer; 194 | margin: 0 50px 4px 0; 195 | min-height: 18px; 196 | padding: 8px; 197 | position: relative; 198 | transition: margin-right .1s; 199 | 200 | .card-label-selectable-icon { 201 | position: absolute; 202 | top: 8px; 203 | right: -20px; 204 | } 205 | 206 | &.active:hover, 207 | &.active, 208 | &.active.selected:hover, 209 | &.active.selected { 210 | margin-right: 38px; 211 | padding-right: 32px; 212 | 213 | .card-label-selectable-icon { 214 | right: 6px; 215 | } 216 | } 217 | 218 | &.active:hover:hover, 219 | &.active:hover, 220 | &.active.selected:hover:hover, 221 | &.active.selected:hover { 222 | margin-right: 38px; 223 | } 224 | 225 | &.selected, 226 | &:hover { 227 | margin-right: 38px; 228 | opacity: .8; 229 | } 230 | } 231 | 232 | .active .card-label-selectable { 233 | 234 | &, 235 | &:hover { 236 | margin-right: 0; 237 | } 238 | 239 | .card-label-selectable-icon { 240 | right: 8px; 241 | } 242 | } 243 | 244 | .card-label-edit-button { 245 | border-radius: 3px; 246 | float: right; 247 | padding: 8px; 248 | 249 | &:hover { 250 | background: #dbdbdb; 251 | } 252 | } 253 | 254 | .card-label-color-select-icon { 255 | left: 14px; 256 | position: absolute; 257 | top: 9px; 258 | } 259 | 260 | .phenom .card-label { 261 | display: inline-block; 262 | font-size: 12px; 263 | height: 14px; 264 | line-height: 13px; 265 | padding: 0 4px; 266 | min-width: 16px; 267 | overflow: ellipsis; 268 | } 269 | 270 | .board-widget .phenom .card-label { 271 | max-width: 130px; 272 | } 273 | -------------------------------------------------------------------------------- /client/views/main/templates.html: -------------------------------------------------------------------------------- 1 | 2 | Libreboard 3 | 4 | 5 | 6 | 7 | 14 | 15 | 18 | 19 | 37 | 38 | 62 | 63 | 73 | 74 | 111 | 112 | 118 | 119 | 122 | 123 | 131 | 132 | 146 | 147 | 165 | 166 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /i18n/ja.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "account-details": "アカウント詳細", 3 | "actions": "操作", 4 | "activity": "Activity", 5 | "activity-archive-card": "archived __cardTitle__.", 6 | "activity-archive-list": "archived __listTitle__", 7 | "activity-create-board": "ボードが作成されました。", 8 | "activity-create-card": "added __cardTitle__ to __listTitle__.", 9 | "activity-create-list": "__listTitle__をボードに追加しました。", 10 | "activity-move-card": "moved __cardTitle__ from __oldListTitle__ to __listTitle__.", 11 | "activity-restore-card": "sent __cardTitle__ to the board.", 12 | "add": "追加", 13 | "add-board": "ボード追加", 14 | "add-card": "カード追加...", 15 | "add-list": "リスト追加...", 16 | "add-members": "メンバー追加...", 17 | "added": "追加しました", 18 | "admin": "管理", 19 | "admin-desc": "Can view and edit cards, remove members, and change settings for the board.", 20 | "already-have-account-question": "すでにアカウントをお持ちですか?", 21 | "archive": "アーカイブ", 22 | "archive-all": "すべてをアーカイブ", 23 | "archive-list": "このリストをアーカイブ", 24 | "archive-title": "ボードからカードを取り除く", 25 | "archived-items": "アーカイブされたアイテム", 26 | "back": "戻る", 27 | "bio": "自己紹介", 28 | "board-list-btn-title": "ボード一覧を見る", 29 | "board-not-found": "ボードが見つかりません", 30 | "board-public-info": "This board will be public.", 31 | "boards": "ボード", 32 | "bucket-example": "Like “Bucket List” for example…", 33 | "cancel": "キャンセル", 34 | "card-archived": "カードはアーカイブされました。", 35 | "card-comments-title": "%s 件のコメントがあります。", 36 | "card-delete-notice": "Deleting is permanent. You will lose all actions associated with this card.", 37 | "card-delete-pop": "All actions will be removed from the activity feed and you won't be able to re-open the card. There is no undo. You can archive a card to remove it from the board and preserve the activity.", 38 | "change-avatar": "アバターの変更", 39 | "change-email": "メールアドレスの変更", 40 | "change-name-initials-bio": "名前、イニシャル、自己紹介の変更", 41 | "change-password": "パスワードの変更", 42 | "change-permissions": "権限の変更...", 43 | "close": "閉じる", 44 | "close-board": "ボードを閉じる", 45 | "close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.", 46 | "close-sidebar-title": "サイドバーを閉じる", 47 | "comment": "コメント", 48 | "comment-placeholder": "コメントする", 49 | "create": "作成", 50 | "create-account": "アカウント作成", 51 | "create-new-account": "新規アカウント作成", 52 | "delete": "削除", 53 | "delete-title": "Delete the card and all history associated with it. It can’t be retrieved.", 54 | "description": "詳細", 55 | "edit": "編集", 56 | "edit-description": "詳細を編集する", 57 | "edit-profile": "プロフィール編集", 58 | "email": "メールアドレス", 59 | "email-or-username": "メールアドレスまたはユーザー名", 60 | "email-placeholder": "例:doc@frankenstein.com", 61 | "filter-cards": "Filter Cards", 62 | "filter-clear": "Clear filter.", 63 | "filter-on": "Filtering is on.", 64 | "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.", 65 | "fullname": "フルネーム", 66 | "gloabal-search": "Global Search", 67 | "header-logo-title": "自分のボードページに戻る。", 68 | "home": "ホーム", 69 | "home-button": "サインアップー無料!", 70 | "home-login": "またはログイン", 71 | "in-list": "in list", 72 | "info": "Infos", 73 | "joined": "joined", 74 | "labels": "ラベル", 75 | "labels-title": "カードのラベルを変更する", 76 | "label-create": "ラベル作成", 77 | "label-delete-pop": "There is no undo. This will remove this label from all cards and destroy its history.", 78 | "label-default": "%s label (default)", 79 | "last-admin-desc": "You can’t change roles because there must be at least one admin.", 80 | "leave-board": "ボードから移動...", 81 | "link-card": "Link to this card", 82 | "list-move-cards": "このリスト内の全カードを移動...", 83 | "list-archive-cards": "このリスト内の全カードをアーカイブ...", 84 | "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click “Menu” > “Archived Items”.", 85 | "log-in": "ログイン", 86 | "log-out": "ログアウト", 87 | "members": "メンバー", 88 | "members-title": "Add or remove members of the board from the card.", 89 | "menu": "メニュー", 90 | "modal-close-title": "ダイアログを閉じる", 91 | "my-boards": "自分のボード", 92 | "name": "名前", 93 | "name-placeholder": "例:Dr.フランケンシュタイン", 94 | "new-here-question": "New here?", 95 | "normal": "Normal", 96 | "normal-desc": "Can view and edit cards. Can't change settings.", 97 | "no-boards": "ボードがありません。", 98 | "no-results": "該当するものはありません", 99 | "notifications-title": "通知", 100 | "optional": "任意", 101 | "page-maybe-private": "This page may be private. You may be able to view it by logging in.", 102 | "page-not-found": "ページが見つかりません。", 103 | "password": "パスワード", 104 | "password-placeholder": "例: ••••••••••••••••", 105 | "private": "プライベート", 106 | "private-desc": "このボードはプライベートです。ボードメンバーのみが閲覧・編集可能です。", 107 | "profile": "プロフィール", 108 | "public": "公開", 109 | "public-desc": "This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit.", 110 | "remove-from-board": "ボードから取り除く...", 111 | "remove-member": "メンバーを外す", 112 | "remove-member-from-card": "カードから取り除く", 113 | "remove-member-pop": "Remove __name__ (__username__) from __boardTitle__? The member will be removed from all cards on this board. They will receive a notification.", 114 | "rename": "名前変更", 115 | "save": "保存", 116 | "search": "検索", 117 | "search-member-desc": "Search for a person in LibreBoard by name or email address, or enter an email address to invite someone new.", 118 | "search-title": "Search for boards, cards, members, and organizations.", 119 | "select-color": "色を選択", 120 | "send-to-board": "ボードへ送る", 121 | "send-to-board-title": "Send the card back to the board.", 122 | "settings": "設定", 123 | "share-and-more": "共有、その他", 124 | "share-and-more-title": "共有、印刷、エクスポートおよび削除などのオプション", 125 | "show-sidebar": "サイドバーを表示", 126 | "sign-up": "サインアップ", 127 | "star-board-title": "ボードにスターをつけると自分のボード一覧のトップに表示されます。", 128 | "starred-boards": "スターのついたボード", 129 | "subscribe": "購読", 130 | "team": "チーム", 131 | "title": "タイトル", 132 | "user-profile-not-found": "プロフィールが見つかりません。", 133 | "username": "ユーザー名", 134 | "warning-signup": "無料でサインアップ", 135 | "cardLabelsPopup-title": "ラベル", 136 | "cardMembersPopup-title": "メンバー", 137 | "cardMorePopup-title": "More", 138 | "cardDeletePopup-title": "カードを削除しますか?", 139 | "boardChangeTitlePopup-title": "ボード名の変更", 140 | "boardChangePermissionPopup-title": "Change Visibility", 141 | "addMemberPopup-title": "メンバー", 142 | "closeBoardPopup-title": "ボードを閉じますか?", 143 | "removeMemberPopup-title": "メンバーを外しますか?", 144 | "createBoardPopup-title": "ボードの作成", 145 | "listActionPopup-title": "操作一覧", 146 | "editLabelPopup-title": "ラベルの変更", 147 | "listMoveCardsPopup-title": "リスト内のすべてのカードを移動する", 148 | "listArchiveCardsPopup-title": "このリスト内の善カードをアーカイブしますか?", 149 | "createLabelPopup-title": "ラベルの作成", 150 | "deleteLabelPopup-title": "ラベルを削除しますか?", 151 | "changePermissionsPopup-title": "パーミッションの変更" 152 | } -------------------------------------------------------------------------------- /client/views/users/member.styl: -------------------------------------------------------------------------------- 1 | .inline-member { 2 | cursor: pointer; 3 | } 4 | 5 | .inline-member-av { 6 | display: inline-block; 7 | height: 14px; 8 | margin: 0 0 -2px 0; 9 | position: relative; 10 | width: 14px; 11 | } 12 | 13 | .member { 14 | background-color: #dbdbdb; 15 | border-radius: 3px; 16 | color: #4d4d4d; 17 | display: block; 18 | float: left; 19 | height: 50px; 20 | margin: 0 4px 4px 0; 21 | overflow: hidden; 22 | position: relative; 23 | width: 50px; 24 | cursor: pointer; 25 | user-select: none; 26 | z-index: 1; 27 | text-decoration: none; 28 | 29 | .avatar-initials { 30 | background-color: #dbdbdb; 31 | color: #444444; 32 | font-weight: bold; 33 | max-width: 100%; 34 | max-height: 100%; 35 | font-size: 16px; 36 | line-height: 50px; 37 | width: 50px; 38 | } 39 | 40 | .avatar-image { 41 | max-width: 100%; 42 | max-height: 100%; 43 | } 44 | 45 | &.extra-small { 46 | .avatar-initials { 47 | font-size: 9px; 48 | width: 18px; 49 | height: 18px; 50 | line-height: 18px; 51 | } 52 | 53 | .avatar-image { 54 | width: 18px; 55 | height: 18px; 56 | } 57 | } 58 | 59 | &.small { 60 | width: 30px; 61 | height: 30px; 62 | 63 | .avatar-initials { 64 | font-size: 12px; 65 | line-height: 30px; 66 | } 67 | } 68 | 69 | &.large { 70 | height: 85px; 71 | line-height: 85px; 72 | width: 85px; 73 | 74 | .avatar { 75 | width: 85px; 76 | height: 85px; 77 | .avatar-initials { 78 | font-size: 16px; 79 | font-weight: 700; 80 | line-height: 85px; 81 | width: 85px; 82 | } 83 | } 84 | } 85 | } 86 | 87 | .member:hover .member-avatar { 88 | opacity: .75; 89 | } 90 | 91 | .member.inline { 92 | float: none; 93 | display: inline-block; 94 | } 95 | 96 | .member-initials { 97 | display: block; 98 | font-size: 12px; 99 | font-weight: 700; 100 | height: 30px; 101 | left: 0; 102 | line-height: 30px; 103 | overflow: hidden; 104 | position: absolute; 105 | text-align: center; 106 | top: 0; 107 | width: 100%; 108 | } 109 | 110 | .member-avatar { 111 | height: 30px; 112 | width: 30px; 113 | } 114 | 115 | .member-type { 116 | bottom: 0; 117 | height: 9px; 118 | position: absolute; 119 | left: 1px; 120 | width: 9px; 121 | z-index: 3; 122 | } 123 | 124 | .member-type.admin { 125 | background-size: 100%; 126 | } 127 | 128 | .member-status { 129 | background-color: #b3b3b3; 130 | border: 1px solid #fff; 131 | border-radius: 5px; 132 | bottom: 1px; 133 | box-shadow: 0 1px 0 rgba(0, 0, 0, .3); 134 | height: 6px; 135 | position: absolute; 136 | right: 1px; 137 | width: 6px; 138 | z-index: 3; 139 | } 140 | 141 | .member-status.active { 142 | background: #64c464; 143 | background: linear-gradient(to bottom, #64c464 0, #3fa63f 100%); 144 | border-color: #daf1da; 145 | } 146 | 147 | .member-status.idle { 148 | background: #e4e467; 149 | background: linear-gradient(to bottom, #e4e467 0, #c3c322 100%); 150 | border-color: #f7f7d4; 151 | } 152 | 153 | .member-status.disconnected { 154 | background: #bdbdbd; 155 | background: linear-gradient(to bottom, #bdbdbd 0, #9e9e9e 100%); 156 | border-color: #ededed; 157 | } 158 | 159 | .member-no-menu:hover { 160 | cursor: default; 161 | } 162 | 163 | .member-no-menu:hover .member-avatar { 164 | opacity: 1; 165 | } 166 | 167 | .member-deactivated { 168 | background: #aaa; 169 | opacity: .4; 170 | } 171 | 172 | .member-deactivated .member-initials { 173 | color: #111; 174 | } 175 | 176 | .member-deactivated .member-avatar { 177 | opacity: .2; 178 | } 179 | 180 | .member-virtual { 181 | background: #e3e3e3; 182 | } 183 | 184 | .member-virtual .status { 185 | display: none; 186 | } 187 | 188 | .member-virtual .member-initials { 189 | color: #777; 190 | } 191 | 192 | .member-large { 193 | background-color: #dbdbdb; 194 | border: 1px solid #c2c2c2; 195 | border-radius: 3px; 196 | display: block; 197 | height: 50px; 198 | overflow: hidden; 199 | position: relative; 200 | width: 50px; 201 | z-index: 1; 202 | } 203 | 204 | .member-large .member-initials { 205 | display: block; 206 | font-size: 16px; 207 | font-weight: 700; 208 | height: 50px; 209 | left: 0; 210 | line-height: 50px; 211 | overflow: hidden; 212 | position: absolute; 213 | right: 0; 214 | text-align: center; 215 | top: 0; 216 | } 217 | 218 | .member-large .av-btn { 219 | border-radius: 3px; 220 | color: #fff; 221 | font-size: 12px; 222 | font-weight: 700; 223 | height: 50px; 224 | left: 0; 225 | line-height: 50px; 226 | opacity: 0; 227 | position: absolute; 228 | text-align: center; 229 | top: 0; 230 | width: 50px; 231 | z-index: 2; 232 | } 233 | 234 | .member-large .member-avatar { 235 | border-radius: 3px; 236 | height: 50px; 237 | width: 50px; 238 | } 239 | 240 | .member-large:hover .av-btn.change { 241 | opacity: 1; 242 | background: rgba(0, 0, 0, .5); 243 | } 244 | 245 | 246 | .avatar-option { 247 | border-radius: 3px; 248 | display: block; 249 | height: 42px; 250 | padding: 4px 0; 251 | margin: 8px 0; 252 | position: relative; 253 | text-decoration: none; 254 | } 255 | 256 | .avatar-option:hover { 257 | background: #dcdcdc; 258 | } 259 | 260 | .avatar-option .pic { 261 | border: 1px solid #ccc; 262 | border-radius: 3px; 263 | height: 40px; 264 | left: 4px; 265 | position: absolute; 266 | top: 4px; 267 | width: 40px; 268 | } 269 | 270 | .avatar-option .pic .member-initials { 271 | font-size: 16px; 272 | line-height: 40px; 273 | height: 40px; 274 | width: 40px; 275 | } 276 | 277 | .avatar-option .text { 278 | line-height: 42px; 279 | font-size: 16px; 280 | font-weight: 700; 281 | margin-left: 56px; 282 | text-decoration: underline; 283 | } 284 | 285 | .avatar-option .text .icon-sm { 286 | display: none; 287 | position: absolute; 288 | top: 16px; 289 | right: 10px; 290 | } 291 | 292 | .avatar-option.active .text .icon-sm { 293 | display: block; 294 | } 295 | 296 | .member-profile-info { 297 | margin-bottom: 8px; 298 | } 299 | 300 | .member-profile-info .info { 301 | margin-left: 40px; 302 | } 303 | 304 | .member-profile-info .info h3 a { 305 | text-decoration: none; 306 | } 307 | 308 | .member-profile-info .info h3 a:hover { 309 | text-decoration: underline; 310 | } 311 | 312 | .member-account-session { 313 | margin: 18px 0; 314 | } 315 | 316 | .member-login { 317 | background: #f0f0f0; 318 | border: 1px solid #dcdcdc; 319 | border-radius: 3px; 320 | box-sizing: border-box; 321 | display: inline-block; 322 | margin: 0 1% 1% 0; 323 | overflow: hidden; 324 | padding: 8px 10px; 325 | position: relative; 326 | width: 32%; 327 | word-wrap: break-word; 328 | vertical-align: top; 329 | } 330 | 331 | .member-login-remove { 332 | cursor: pointer; 333 | padding: 8px; 334 | position: absolute; 335 | right: 0; 336 | top: 0; 337 | } 338 | 339 | .small-window .member-login { 340 | margin: 0 0 8px; 341 | width: 100%; 342 | } 343 | 344 | .manage-member-section { 345 | margin-top: 12px; 346 | } 347 | 348 | .manage-member-section h4 { 349 | border-bottom: 1px solid #dcdcdc; 350 | padding-bottom: 4px; 351 | } 352 | -------------------------------------------------------------------------------- /client/views/boards/body.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | .body-board-view { 3 | background-size: cover; 4 | overflow: hidden; 5 | 6 | #header { 7 | background: rgba(0, 0, 0, .15); 8 | } 9 | 10 | &.body-custom-board-background-tiled { 11 | background-size: auto; 12 | background-repeat: repeat; 13 | } 14 | } 15 | 16 | .body-default-header #header { 17 | background: #27709b; 18 | background: linear-gradient(to bottom, #27709b 0, #24688f 100%); 19 | box-shadow: 0 1px 2px rgba(0, 0, 0, .1), 0 1px 0 rgba(0, 0, 0, .1); 20 | } 21 | 22 | .body-custom-board-background .board-header-btn:not(.no-edit):hover, 23 | .body-custom-board-background .sidebar-show-btn:hover { 24 | background: rgba(0, 0, 0, .3); 25 | } 26 | 27 | .body-light-board-background #header { 28 | background: rgba(0, 0, 0, .35); 29 | } 30 | 31 | .body-light-board-background .board-header-btn:not(.board-header-btn-filter-indicator):not(.board-header-btn-enabled), 32 | .body-light-board-background .sidebar-show-btn { 33 | background: transparent; 34 | color: rgba(0, 0, 0, .7); 35 | 36 | .board-header-btn-icon, 37 | .icon-sm { 38 | color: rgba(0, 0, 0, .7); 39 | } 40 | 41 | &:hover { 42 | background: rgba(0, 0, 0, .1); 43 | } 44 | } 45 | 46 | .body-light-board-background .board-header-btn-enabled, .body-light-board-background .board-header-btn-enabled:hover, 47 | .body-light-board-background .board-header-btn-enabled .board-header-btn-icon, 48 | .body-light-board-background .board-header-btn-enabled:hover .board-header-btn-icon { 49 | color: rgba(0, 0, 0, .7); 50 | } 51 | 52 | .board-canvas { 53 | margin-right: 264px; 54 | position: relative; 55 | transition-property: margin; 56 | transition-duration: .1s; 57 | transition-timing-function: ease-in; 58 | } 59 | 60 | #board { 61 | align-items: flex-start; 62 | display: flex; 63 | flex-direction: row; 64 | margin-bottom: 10px; 65 | overflow-x: auto; 66 | overflow-y: hidden; 67 | padding-bottom: 10px; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | bottom: 0; 72 | left: 0; 73 | 74 | &.hide { 75 | display: none; 76 | } 77 | 78 | &::-webkit-scrollbar { 79 | height: 13px; 80 | width: 13px; 81 | } 82 | 83 | &::-webkit-scrollbar-thumb:vertical, 84 | &::-webkit-scrollbar-thumb:horizontal { 85 | background: rgba(255, 255, 255, .4); 86 | } 87 | 88 | &::-webkit-scrollbar-track-piece { 89 | background: rgba(0, 0, 0, .15); 90 | } 91 | 92 | &::-webkit-scrollbar-button { 93 | display: block; 94 | height: 5px; 95 | width: 5px; 96 | } 97 | } 98 | 99 | .body-no-webkit-scrollbars:not(.firefox) #board { 100 | padding-bottom: 30px; 101 | } 102 | 103 | .version-tag { 104 | color: #fff; 105 | height: 30px; 106 | line-height: 15px; 107 | font-size: 12px; 108 | text-align: right; 109 | margin: 10px; 110 | white-space: pre; 111 | opacity: .5; 112 | pointer-events: none; 113 | position: absolute; 114 | right: 0; 115 | bottom: 0; 116 | z-index: 0; 117 | } 118 | 119 | .extra-large-window .board-wrapper.sidebar-display-wide { 120 | 121 | .board-canvas { 122 | margin-right: 464px; 123 | } 124 | 125 | .board-widgets { 126 | width: 446px; 127 | } 128 | 129 | .board-header { 130 | padding-right: 500px; 131 | } 132 | 133 | .archive-controls { 134 | clear: both; 135 | 136 | .archive-search input { 137 | width: auto; 138 | } 139 | 140 | .archive-switch { 141 | float: right; 142 | width: auto; 143 | z-index: 1; 144 | } 145 | 146 | } 147 | .archive-content .show-more { 148 | clear: both; 149 | } 150 | 151 | .board-backgrounds-list .board-background-select, 152 | .board-backgrounds-list .board-background-still-uploading { 153 | display: inline-block; 154 | padding: 0 6px 12px 0; 155 | width: 33%; 156 | 157 | &:nth-child(3n - 1) { 158 | padding: 0 6px 12px 6px; 159 | } 160 | 161 | &:nth-child(3n) { 162 | padding: 0 0 12px 6px; 163 | } 164 | } 165 | 166 | 167 | .archived-list-card { 168 | float: left; 169 | width: 50%; 170 | padding: 0 0 0 0; 171 | min-height: 80px; 172 | 173 | &:nth-child(2n+1) { 174 | clear: left; 175 | } 176 | } 177 | } 178 | 179 | .archive-controls .archive-search { 180 | margin: 6px 0 12px 0; 181 | } 182 | 183 | .archive-controls .archive-switch { 184 | width: 100%; 185 | padding: 6px 10px; 186 | margin: 0; 187 | } 188 | 189 | .board-widgets { 190 | background: #f0f0f0; 191 | border-top-left-radius: 3px; 192 | border-top-right-radius: 0; 193 | border-bottom-right-radius: 0; 194 | border-bottom-left-radius: 0; 195 | box-shadow: 0 0 6px rgba(0, 0, 0, .4); 196 | bottom: 0; 197 | padding: 12px 6px 0 12px; 198 | position: absolute; 199 | right: 0; 200 | top: 0; 201 | transition-property: transform, width; 202 | transition-duration: .1s; 203 | transition-timing-function: ease-in; 204 | transform: translateX(0); 205 | width: 246px; 206 | z-index: 5; 207 | 208 | input[type="text"] { 209 | width: 100%; 210 | } 211 | 212 | hr { 213 | margin: 8px 0; 214 | } 215 | 216 | .board-widgets-title { 217 | position: relative; 218 | transition-property: opacity; 219 | transition-duration: .2s; 220 | transition-timing-function: ease-in; 221 | 222 | .text { 223 | float: left; 224 | line-height: 14px; 225 | padding: 8px; 226 | } 227 | 228 | &.in-frame { 229 | height: 46px; 230 | opacity: 1; 231 | overflow: visible; 232 | } 233 | 234 | .board-sidebar-back-btn { 235 | background: #dcdcdc; 236 | border-top-left-radius: 0; 237 | border-top-right-radius: 3px; 238 | border-bottom-right-radius: 3px; 239 | border-bottom-left-radius: 0; 240 | display: block; 241 | font-weight: 700; 242 | float: left; 243 | line-height: 14px; 244 | margin: 0 0 0 4px; 245 | padding: 8px 8px 8px 6px; 246 | position: relative; 247 | text-decoration: none; 248 | 249 | .left-arrow { 250 | border-bottom: 15px solid transparent; 251 | border-right: 10px solid #dcdcdc; 252 | border-top: 15px solid transparent; 253 | display: block; 254 | height: 0; 255 | position: absolute; 256 | left: -10px; 257 | top: 0; 258 | width: 0; 259 | } 260 | } 261 | 262 | .board-sidebar-back-btn:hover { 263 | background: #ccc; 264 | 265 | .left-arrow { 266 | border-right-color: #ccc; 267 | } 268 | } 269 | } 270 | 271 | .board-widgets-content-wrapper { 272 | overflow: hidden; 273 | } 274 | 275 | .board-widgets-content { 276 | padding-right: 6px; 277 | overflow-x: hidden; 278 | overflow-y: auto; 279 | transition-property: transform, opacity; 280 | transition-duration: .12s; 281 | transition-timing-function: ease-in; 282 | transform: translateX(0); 283 | 284 | &.left-of-frame { 285 | overflow: hidden; 286 | transform: translateX(-280px); 287 | } 288 | 289 | &.right-of-frame { 290 | overflow: hidden; 291 | transform: translateX(280px); 292 | } 293 | 294 | &.default { 295 | overflow: hidden; 296 | 297 | &.short { 298 | overflow-y: auto; 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /i18n/tr.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "account-details": "Hesap Ayrıntıları", 3 | "actions": "İşlemler", 4 | "activity": "Etkinlik", 5 | "activity-archive-card": "__cardTitle__ arşivledi.", 6 | "activity-archive-list": "__listTitle__ arşivledi", 7 | "activity-create-board": "bu panoyu oluşturdu.", 8 | "activity-create-card": "__listTitle__ listesine __cardTitle__ ekledi.", 9 | "activity-create-list": "bu panoya __listTitle__ eklendi.", 10 | "activity-move-card": "__oldListTitle__ > __listTitle__ __cardTitle__ taşıdı.", 11 | "activity-restore-card": "sent __cardTitle__ to the board.", 12 | "add": "Ekle", 13 | "add-board": "Yeni bir pano ekle", 14 | "add-card": "Bir kart ekle...", 15 | "add-list": "Bir liste ekle...", 16 | "add-members": "Üye Ekle...", 17 | "added": "Added", 18 | "admin": "Yönetici", 19 | "admin-desc": "Kartları görüntüler ve düzenler, üyeleri çıkarır ve pano ayarlarını değiştirir.", 20 | "already-have-account-question": "Bir hesabın mı var?", 21 | "archive": "Arşiv", 22 | "archive-all": "Tümünü Arşivle", 23 | "archive-list": "Bu listeyi arşivle", 24 | "archive-title": "Remove the card from the board.", 25 | "archived-items": "Arşivlenmiş Öğeler", 26 | "back": "Geri", 27 | "bio": "Biyografi", 28 | "board-list-btn-title": "Pano listesini görüntüle", 29 | "board-not-found": "Pano bulunamadı", 30 | "board-public-info": "This board will be public.", 31 | "boards": "Panolar", 32 | "bucket-example": "Örnek olarak “Yapılacaklar Listesi” gibi…", 33 | "cancel": "İptal", 34 | "card-archived": "Bu kart arşivlendi.", 35 | "card-comments-title": "This card has %s comment.", 36 | "card-delete-notice": "Silme işlemi kalıcıdır. Bu kartla ilişkili tüm eylemleri kaybedersiniz.", 37 | "card-delete-pop": "Tüm eylemler etkinlik beslemesinden kaldırılacaktır ve kartı yeniden açmak mümkün olmayacaktır. Geri dönüşü yok. Panodan çıkarmak ve etkinlik kayıtlarını korumak için kartı arşivleyebilirsin.", 38 | "change-avatar": "Avatar Değiştir", 39 | "change-email": "E-posta Değiştir", 40 | "change-name-initials-bio": "Change Name, Initials, or Bio", 41 | "change-password": "Parola Değiştir", 42 | "change-permissions": "Yetkileri değiştir...", 43 | "close": "Kapat", 44 | "close-board": "Panoyu Kapat...", 45 | "close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.", 46 | "close-sidebar-title": "Pano kenar çubuğunu kapat.", 47 | "comment": "Yorum Gönder", 48 | "comment-placeholder": "Bir yorum yaz...", 49 | "create": "Oluştur", 50 | "create-account": "Bir Hesap Oluştur", 51 | "create-new-account": "Yeni bir hesap oluştur", 52 | "delete": "Sil", 53 | "delete-title": "\n", 54 | "description": "Açıklama", 55 | "edit": "Düzenle", 56 | "edit-description": "Açıklamayı düzenle...", 57 | "edit-profile": "Bilgilerini düzenle", 58 | "email": "E-posta", 59 | "email-or-username": "E-posta veya kullanıcı adı", 60 | "email-placeholder": "örn., doc@frankenstein.com", 61 | "filter-cards": "Filter Cards", 62 | "filter-clear": "Clear filter.", 63 | "filter-on": "Filtering is on.", 64 | "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.", 65 | "fullname": "Ad Soyad", 66 | "gloabal-search": "Global Search", 67 | "header-logo-title": "Panolar sayfanıza geri dön.", 68 | "home": "Home", 69 | "home-button": "Kaydol—Ücretsiz!", 70 | "home-login": "Veya oturum aç", 71 | "in-list": ", listesinde", 72 | "info": "Infos", 73 | "joined": "joined", 74 | "labels": "Etiketler", 75 | "labels-title": "Change the labels for the card.", 76 | "label-create": "Yeni bir etiket oluştur", 77 | "label-delete-pop": "Geri dönüşü yok. Tüm kartlardan bu etiket kaldırılacaktır ve geçmişini yok edecektir.", 78 | "label-default": "%s label (default)", 79 | "last-admin-desc": "Rolleri değiştiremezsiniz çünkü burada en az bir yönetici olmalıdır.", 80 | "leave-board": "Leave Board…", 81 | "link-card": "Link to this card", 82 | "list-move-cards": "Bu Listedeki Tüm Kartları Taşı...", 83 | "list-archive-cards": "Bu Listedeki Tüm Kartlar Arşivle...", 84 | "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click “Menu” > “Archived Items”.", 85 | "log-in": "Oturum Aç", 86 | "log-out": "Oturum Kapat", 87 | "members": "Üyeler", 88 | "members-title": "Add or remove members of the board from the card.", 89 | "menu": "Menü", 90 | "modal-close-title": "Bu iletişim penceresini kapatın.", 91 | "my-boards": "Panolarım", 92 | "name": "Kullanıcı Adı", 93 | "name-placeholder": "örn., Dr. Frankenstein", 94 | "new-here-question": "Burada yeni misin?", 95 | "normal": "Normal", 96 | "normal-desc": "Kartları görüntüler ve düzenler. Ayarları değiştiremez.", 97 | "no-boards": "Pano yok.", 98 | "no-results": "Sonuç yok", 99 | "notifications-title": "Bildirimler", 100 | "optional": "isteğe bağlı", 101 | "page-maybe-private": "Bu sayfa özel olabilir. Oturum açarak görülebilir.", 102 | "page-not-found": "Sayda bulunamadı.", 103 | "password": "Parola", 104 | "password-placeholder": "örn., ••••••••••••••••", 105 | "private": "Özel", 106 | "private-desc": "Bu pano özel. Sadece panoya ekli kişiler görüntüleyebilir ve düzenleyebilir.", 107 | "profile": "Kullanıcı Sayfası", 108 | "public": "Genel", 109 | "public-desc": "This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit.", 110 | "remove-from-board": "Panodan çıkar...", 111 | "remove-member": "Üyeyi Çıkar", 112 | "remove-member-from-card": "Karttan Çıkar", 113 | "remove-member-pop": "__boardTitle__ panosundan __name__ (__username__) çıkarılsın mı? Üye, bu panodaki tüm kartlardan çıkarılacak ve bir bildirim alacak.", 114 | "rename": "Ad değiştir", 115 | "save": "Kaydet", 116 | "search": "Search", 117 | "search-member-desc": "LibreBoard'da, bir kişiyi adı veya e-posta adresi ile arayın ya da yeni birini davet etmek için bir e-posta adresi girin.", 118 | "search-title": "Pano, kart, üye ve örgütleri ara.", 119 | "select-color": "Bir renk seç", 120 | "send-to-board": "Panoya gönder", 121 | "send-to-board-title": "Kartı, panoya geri gönder.", 122 | "settings": "Ayarlar", 123 | "share-and-more": "Paylaş ve daha...", 124 | "share-and-more-title": "Birçok seçenek; paylaş, bastır, dışarı aktar ve sil.", 125 | "show-sidebar": "Kenar çubuğunu göster", 126 | "sign-up": "Kaydol", 127 | "star-board-title": "Bu panoyu yıldızlamak için tıkla. Pano listesinin en üstünde gösterilir.", 128 | "starred-boards": "Yıldızlı Panolar", 129 | "subscribe": "Subscribe", 130 | "team": "Takım", 131 | "title": "Başlık", 132 | "user-profile-not-found": "Kullanıcı Sayfası bulunamadı.", 133 | "username": "Kullanıcı adı", 134 | "warning-signup": "Ücretsiz Kaydol", 135 | "cardLabelsPopup-title": "Etiketler", 136 | "cardMembersPopup-title": "Üyeler", 137 | "cardMorePopup-title": "More", 138 | "cardDeletePopup-title": "Kart Silinsin mi?", 139 | "boardChangeTitlePopup-title": "Pano Adı Değiştirme", 140 | "boardChangePermissionPopup-title": "Görünebilirliği Değiştir", 141 | "addMemberPopup-title": "Üyeler", 142 | "closeBoardPopup-title": "Pano Kapatılsın mı?", 143 | "removeMemberPopup-title": "Üyeyi Çıkarmak mı?", 144 | "createBoardPopup-title": "Pano Oluşturma", 145 | "listActionPopup-title": "List Actions", 146 | "editLabelPopup-title": "Etiket Değiştirme", 147 | "listMoveCardsPopup-title": "Listedeki Tüm Kartları Taşıma", 148 | "listArchiveCardsPopup-title": "Bu Listedeki Tüm Kartlar Taşınsın mı?", 149 | "createLabelPopup-title": "Etiket Oluşturma", 150 | "deleteLabelPopup-title": "Etiket Silinsin mi?", 151 | "changePermissionsPopup-title": "Yetkileri Değiştirme" 152 | } -------------------------------------------------------------------------------- /collections/boards.js: -------------------------------------------------------------------------------- 1 | Boards = new Mongo.Collection('boards'); 2 | 3 | Boards.attachSchema(new SimpleSchema({ 4 | title: { 5 | type: String 6 | }, 7 | slug: { 8 | type: String 9 | }, 10 | archived: { 11 | type: Boolean, 12 | defaultValue: false 13 | }, 14 | createdAt: { 15 | type: Date, 16 | denyUpdate: true, 17 | autoValue: function() { 18 | if (this.isInsert) 19 | return new Date(); 20 | } 21 | }, 22 | // XXX Inconsistent field naming 23 | modifiedAt: { 24 | type: Date, 25 | denyInsert: true, 26 | optional: true, 27 | autoValue: function() { 28 | if (this.isUpdate) 29 | return new Date(); 30 | } 31 | }, 32 | // De-normalized label system 33 | 'labels.$._id': { 34 | // We don't specify that this field must be unique in the board because 35 | // that will cause performance penalties and is not necessary because 36 | // this field is always set on the server. 37 | // XXX Actually if we create a new label, the `_id` is set on the client 38 | // without being overwritten by the server, could it be a problem? 39 | type: String 40 | }, 41 | 'labels.$.name': { 42 | type: String, 43 | optional: true 44 | }, 45 | 'labels.$.color': { 46 | type: String 47 | }, 48 | // XXX We might want to maintain more informations under the member 49 | // sub-documents like an `isActive` boolean (so we can keep a trace of 50 | // former members) or de-normalized meta-data (the data the joined the 51 | // board, the number of contributions, etc.). 52 | 'members.$.userId': { 53 | type: String 54 | }, 55 | 'members.$.isAdmin': { 56 | type: Boolean 57 | }, 58 | permission: { 59 | type: String, 60 | allowedValues: ['public', 'private'] 61 | }, 62 | 'background.type': { 63 | type: String, 64 | allowedValues: ['color'] 65 | }, 66 | 'background.color': { 67 | // It's important to be strict about what we accept here, because if 68 | // certain malicious data are inserted this could lead to XSS injections 69 | // since we display this variable in a