├── server ├── fixtures.js └── publication.coffee ├── .meteor ├── .gitignore ├── release └── packages ├── client ├── coffee │ ├── main.coffee │ ├── config.coffee │ ├── errors.coffee │ └── layout.coffee ├── main.html ├── views │ ├── loading.html │ ├── access_denied.html │ ├── layout.html │ ├── errors.html │ └── header.html ├── features │ ├── comments │ │ ├── helpers │ │ │ └── comments_helper.coffee │ │ ├── views │ │ │ ├── comment.html │ │ │ └── comments_create.html │ │ └── scripts │ │ │ └── comments_create.coffee │ └── posts │ │ ├── views │ │ ├── posts_index.html │ │ ├── posts_show.html │ │ ├── post.html │ │ ├── posts_edit.html │ │ └── posts_submit.html │ │ ├── helpers │ │ └── posts_helper.coffee │ │ └── scripts │ │ ├── posts_create.coffee │ │ └── posts_edit.coffee └── sass │ └── style.css ├── lib ├── permissions.coffee └── router.coffee ├── packages └── .gitignore ├── smart.json ├── collections ├── errors.coffee ├── comments.coffee └── posts.coffee └── smart.lock /server/fixtures.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | 0.8.1.2 2 | -------------------------------------------------------------------------------- /client/coffee/main.coffee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | Microscope -------------------------------------------------------------------------------- /client/views/loading.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /lib/permissions.coffee: -------------------------------------------------------------------------------- 1 | @ownsDocument = (userId, doc) -> 2 | doc && doc.userId == userId 3 | -------------------------------------------------------------------------------- /client/coffee/config.coffee: -------------------------------------------------------------------------------- 1 | Accounts.ui.config 2 | passwordSignupFields: 'USERNAME_ONLY' 3 | -------------------------------------------------------------------------------- /client/coffee/errors.coffee: -------------------------------------------------------------------------------- 1 | Template.errors.helpers 2 | errors: -> 3 | Errors.find() 4 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | /iron-router 2 | /blaze-layout 3 | /spin 4 | /accounts-ui-bootstrap-dropdown 5 | -------------------------------------------------------------------------------- /client/coffee/layout.coffee: -------------------------------------------------------------------------------- 1 | Template.layout.helpers 2 | pageTitle: -> 3 | Session.get('pageTitle') 4 | -------------------------------------------------------------------------------- /server/publication.coffee: -------------------------------------------------------------------------------- 1 | Meteor.publish 'posts', -> 2 | Posts.find() 3 | 4 | Meteor.publish 'comments', -> 5 | Comments.find() 6 | -------------------------------------------------------------------------------- /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "iron-router": {}, 4 | "spin": {}, 5 | "accounts-ui-bootstrap-dropdown": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/features/comments/helpers/comments_helper.coffee: -------------------------------------------------------------------------------- 1 | Template.comment.helpers 2 | submittedText: -> 3 | new Date(@submitted).toString() 4 | -------------------------------------------------------------------------------- /client/views/access_denied.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/features/posts/views/posts_index.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/views/layout.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /collections/errors.coffee: -------------------------------------------------------------------------------- 1 | @Errors = new Meteor.Collection(null) 2 | 3 | 4 | @addError = (message) -> 5 | Errors.insert(message: message, seen: false) 6 | 7 | @clearErrors = -> 8 | Errors.remove({}) 9 | -------------------------------------------------------------------------------- /client/features/comments/views/comment.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.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 | standard-app-packages 7 | bootstrap 8 | coffeescript 9 | stylus 10 | iron-router 11 | spin 12 | accounts-ui-bootstrap-dropdown 13 | accounts-password 14 | -------------------------------------------------------------------------------- /client/features/posts/views/posts_show.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /client/views/errors.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /client/features/posts/helpers/posts_helper.coffee: -------------------------------------------------------------------------------- 1 | Template.posts_index.helpers 2 | posts: -> Posts.find({}, {sort: {submitted: -1}}) 3 | 4 | 5 | Template.posts_show.helpers 6 | comments: -> 7 | Comments.find(postId: @_id).fetch() 8 | 9 | 10 | Template.post.helpers 11 | domain: -> 12 | a = document.createElement('a') 13 | a.href = @url 14 | a.hostname 15 | 16 | ownPost: -> 17 | @userId == Meteor.userId() 18 | -------------------------------------------------------------------------------- /client/features/comments/scripts/comments_create.coffee: -------------------------------------------------------------------------------- 1 | Template.comments_create.events 2 | 'submit form': (e, template) -> 3 | e.preventDefault() 4 | 5 | comment = 6 | body: $(e.target).find('[name=body]').val() 7 | postId: template.data._id 8 | 9 | Meteor.call('comment_create', comment, (err, commentId) -> 10 | if err 11 | Errors.insert(message: err.reason) 12 | else 13 | $(e.target).find('[name=body]').val('') 14 | ) 15 | -------------------------------------------------------------------------------- /client/features/posts/scripts/posts_create.coffee: -------------------------------------------------------------------------------- 1 | Template.posts_create.events 2 | 'submit form': (e) -> 3 | e.preventDefault() 4 | post = 5 | url: $(e.target).find('[name=url]').val() 6 | title: $(e.target).find('[name=title]').val() 7 | message: $(e.target).find('[name=message]').val() 8 | 9 | Meteor.call('post_create', post, (err, id) -> 10 | if err 11 | addError(err.reason) 12 | else 13 | Router.go('posts_index', {_id: id}) 14 | ) 15 | -------------------------------------------------------------------------------- /client/features/posts/views/post.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /client/features/comments/views/comments_create.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /client/features/posts/scripts/posts_edit.coffee: -------------------------------------------------------------------------------- 1 | Template.posts_edit.events 2 | 'submit form': (e) -> 3 | e.preventDefault() 4 | 5 | postProperties = 6 | url: $(e.target).find('[name=url]').val(), 7 | title: $(e.target).find('[name=title]').val() 8 | 9 | Posts.update(@_id, {$set: postProperties}, (err) -> 10 | if err 11 | alert(err.reason) 12 | else 13 | Router.go('posts_index') 14 | ) 15 | 16 | 'click .delete': (e) -> 17 | e.preventDefault() 18 | 19 | if confirm('Delete this post?') 20 | Posts.remove(@._id) 21 | Router.go('posts_index') 22 | -------------------------------------------------------------------------------- /client/views/header.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /collections/comments.coffee: -------------------------------------------------------------------------------- 1 | @Comments = new Meteor.Collection('comments') 2 | 3 | 4 | Meteor.methods 5 | comment_create: (commentAttrs) -> 6 | user = Meteor.user() 7 | post = Posts.findOne(commentAttrs.postId) 8 | 9 | unless user 10 | throw new Meteor.Error(401, 'You need to login to post') 11 | 12 | unless commentAttrs.body 13 | throw new Meteor.Error(422, 'Comment must have text') 14 | 15 | unless post 16 | throw new Meteor.Error(422, 'You must comment on a post') 17 | 18 | comment = _.extend( 19 | _.pick(commentAttrs, 'postId', 'body'), 20 | { 21 | userId: user._id, 22 | author: user.username, 23 | submitted: new Date().getTime() 24 | } 25 | ) 26 | 27 | Posts.update(comment.postId, {$inc: {commentsCount: 1}}) 28 | 29 | Comments.insert(comment) 30 | -------------------------------------------------------------------------------- /client/features/posts/views/posts_edit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/router.coffee: -------------------------------------------------------------------------------- 1 | Router.configure 2 | layoutTemplate: 'layout' 3 | loadingTemplate: 'loading' 4 | waitOn: -> 5 | [ Meteor.subscribe('posts') ] 6 | 7 | Router.map -> 8 | @route('posts_index', path: '/') 9 | @route('posts_show', 10 | path: '/posts/:_id', 11 | waitOn: -> 12 | Meteor.subscribe('comments', @params._id) 13 | data: -> Posts.findOne(@params._id) 14 | ) 15 | @route('posts_create', path: '/posts_create') 16 | @route('posts_edit', 17 | path: '/posts/:_id/edit', 18 | data: -> Posts.findOne(this.params._id) 19 | ) 20 | 21 | 22 | requireLogin = (pause) -> 23 | unless Meteor.user() 24 | if Meteor.loggingIn() 25 | @render('loading') 26 | else 27 | @render('accessDenied') 28 | pause() 29 | 30 | Router.onBeforeAction('loading') 31 | Router.onBeforeAction(requireLogin, {only: 'posts_create'}) 32 | Router.onBeforeAction(-> clearErrors()) 33 | -------------------------------------------------------------------------------- /client/features/posts/views/posts_submit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /collections/posts.coffee: -------------------------------------------------------------------------------- 1 | @Posts = new Meteor.Collection('posts') 2 | 3 | Posts.allow 4 | update: ownsDocument, 5 | remove: ownsDocument 6 | 7 | Posts.deny 8 | update: (userId, post, fieldNames) -> 9 | _.without(fieldNames, 'url', 'title').length > 0 10 | 11 | 12 | Meteor.methods 13 | post_create: (postAttrs) -> 14 | user = Meteor.user() 15 | postWithSameLike = Posts.findOne(url: postAttrs.url) 16 | 17 | unless user 18 | throw new Meteor.Error(401, 'You need to login to post') 19 | 20 | unless postAttrs.title 21 | throw new Meteor.Error(422, 'Posts must have a title') 22 | 23 | if postAttrs.url and postWithSameLike 24 | throw new Meteor.Error(302, 'This link has been posted') 25 | 26 | post = _.extend( 27 | _.pick(postAttrs, 'url', 'title', 'message'), 28 | { 29 | userId: user._id, 30 | author: user.username, 31 | submitted: new Date().getTime(), 32 | commentsCount: 0 33 | } 34 | ) 35 | 36 | Posts.insert(post) 37 | -------------------------------------------------------------------------------- /smart.lock: -------------------------------------------------------------------------------- 1 | { 2 | "meteor": {}, 3 | "dependencies": { 4 | "basePackages": { 5 | "iron-router": {}, 6 | "spin": {}, 7 | "accounts-ui-bootstrap-dropdown": {} 8 | }, 9 | "packages": { 10 | "iron-router": { 11 | "git": "https://github.com/EventedMind/iron-router.git", 12 | "tag": "v0.7.1", 13 | "commit": "d1ffb3f06ea4c112132b030f2eb1a70b81675ecb" 14 | }, 15 | "spin": { 16 | "git": "https://github.com/SachaG/meteor-spin.git", 17 | "tag": "v0.2.2", 18 | "commit": "3f655f0016228e4195acc784c45d3e3a247dc39f" 19 | }, 20 | "accounts-ui-bootstrap-dropdown": { 21 | "git": "https://github.com/erobit/meteor-accounts-ui-bootstrap-dropdown.git", 22 | "tag": "v0.8.3", 23 | "commit": "a7af8a52e4e74db020a69b158a8f992797b8b0b0" 24 | }, 25 | "blaze-layout": { 26 | "git": "https://github.com/EventedMind/blaze-layout.git", 27 | "tag": "v0.2.4", 28 | "commit": "b40e9b0612329288d75cf52ad14a7da64bb8618f" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/sass/style.css: -------------------------------------------------------------------------------- 1 | .grid-block, .main, .post, .comments li, .comment-form { background: #fff; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; padding: 10px; margin-bottom: 10px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); } body { background: #eee; color: #666666; } .navbar { margin-bottom: 10px; } /* line 32, ../sass/style.scss */ .navbar .navbar-inner { -webkit-border-radius: 0px 0px 3px 3px; -moz-border-radius: 0px 0px 3px 3px; -ms-border-radius: 0px 0px 3px 3px; -o-border-radius: 0px 0px 3px 3px; border-radius: 0px 0px 3px 3px; } #spinner { height: 300px; } .post { /* For modern browsers */ /* For IE 6/7 (trigger hasLayout) */ *zoom: 1; position: relative; opacity: 1; } .post:before, .post:after { content: ""; display: table; } .post:after { clear: both; } .post.invisible { opacity: 0; } .post.instant { -webkit-transition: none; -moz-transition: none; -o-transition: none; transition: none; } .post.animate{ -webkit-transition: all 300ms 0ms; -webkit-transition-delay: ease-in; -moz-transition: all 300ms 0ms ease-in; -o-transition: all 300ms 0ms ease-in; transition: all 300ms 0ms ease-in; } .post .upvote { display: block; margin: 7px 12px 0 0; float: left; } .post .post-content { float: left; } .post .post-content h3 { margin: 0; line-height: 1.4; font-size: 18px; } .post .post-content h3 a { display: inline-block; margin-right: 5px; } .post .post-content h3 span { font-weight: normal; font-size: 14px; display: inline-block; color: #aaaaaa; } .post .post-content p { margin: 0; } .post .discuss { display: block; float: right; margin-top: 7px; } .comments { list-style-type: none; margin: 0; } .comments li h4 { font-size: 16px; margin: 0; } .comments li h4 .date { font-size: 12px; font-weight: normal; } .comments li h4 a { font-size: 12px; } .comments li p:last-child { margin-bottom: 0; } .dropdown-menu span { display: block; padding: 3px 20px; clear: both; line-height: 20px; color: #bbb; white-space: nowrap; } .load-more { display: block; -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; background: rgba(0, 0, 0, 0.05); text-align: center; height: 60px; line-height: 60px; margin-bottom: 10px; } .load-more:hover { text-decoration: none; background: rgba(0, 0, 0, 0.1); } --------------------------------------------------------------------------------