├── .meteor
├── .gitignore
├── packages
└── release
├── README.markdown
├── client
├── helpers
│ ├── config.js
│ ├── errors.js
│ ├── handlebars.js
│ └── router.js
├── main.html
├── main.js
├── stylesheets
│ └── style.css
└── views
│ ├── comments
│ ├── comment.html
│ ├── comment.js
│ ├── comment_submit.html
│ └── comment_submit.js
│ ├── includes
│ ├── access_denied.html
│ ├── errors.html
│ ├── errors.js
│ ├── header.html
│ ├── header.js
│ └── loading.html
│ ├── notifications
│ ├── notifications.html
│ └── notifications.js
│ └── posts
│ ├── post_edit.html
│ ├── post_edit.js
│ ├── post_item.html
│ ├── post_item.js
│ ├── post_page.html
│ ├── post_page.js
│ ├── post_submit.html
│ ├── post_submit.js
│ ├── posts_list.html
│ └── posts_list.js
├── collections
├── comments.js
├── notifications.js
└── posts.js
├── lib
└── permissions.js
├── server
├── fixtures.js
└── publications.js
├── smart.json
└── smart.lock
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 | meteorite
3 |
--------------------------------------------------------------------------------
/.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 | preserve-inputs
7 | bootstrap
8 | router
9 | accounts-ui-bootstrap-dropdown
10 | accounts-password
11 | spin
12 | paginated-subscription
13 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | none
2 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # Microscope
2 |
3 | Microscope is a simple social news app that lets you share links, comment, and vote on them.
4 |
5 | It was built with [Meteor](http://meteor.com) as a companion app to [The Meteor Book](http://themeteorbook.com), and is the "little brother" of [Telescope](http://telesc.pe), the (much more complex) open-source social news app that was the inspiration for the book.
6 |
7 | Microscope itself is free and open-source, and is a good example of common Meteor app patterns such as:
8 |
9 | - Routing
10 | - User Accounts
11 | - Notifications
12 | - Errors
13 | - Publications/Subscriptions
14 | - Permissions
15 |
16 | ## This Repository
17 |
18 | The commits to this repository are organized in a very linear fashion, corresponding to progress throughout the book. Commits are tagged in the format `chapterX-Y`, indicating the `Y`th commit of chapter `X`.
19 |
20 | Also, note that as the book focuses on _development_, all CSS is committed in a single commit early on.
21 |
22 | ### Branches
23 |
24 | There are 2 branches in this repository which correspond to advanced code that is covered in sidebars of the book, and outside of the main code progression. They are tagged `sidebarX-Y`, corresponding to the sidebar number in the book.
--------------------------------------------------------------------------------
/client/helpers/config.js:
--------------------------------------------------------------------------------
1 | Accounts.ui.config({
2 | passwordSignupFields: 'USERNAME_ONLY'
3 | });
--------------------------------------------------------------------------------
/client/helpers/errors.js:
--------------------------------------------------------------------------------
1 | // Local (client-only) collection
2 | Errors = new Meteor.Collection(null);
3 |
4 | throwError = function(message) {
5 | Errors.insert({message: message, seen: false})
6 | }
7 |
8 | clearErrors = function() {
9 | Errors.remove({seen: true});
10 | }
--------------------------------------------------------------------------------
/client/helpers/handlebars.js:
--------------------------------------------------------------------------------
1 | Handlebars.registerHelper('pluralize', function(n, thing) {
2 | // fairly stupid pluralizer
3 | if (n === 1) {
4 | return '1 ' + thing;
5 | } else {
6 | return n + ' ' + thing + 's';
7 | }
8 | });
--------------------------------------------------------------------------------
/client/helpers/router.js:
--------------------------------------------------------------------------------
1 | Meteor.Router.add({
2 | '/': {to: 'newPosts', as: 'home'},
3 | '/best': 'bestPosts',
4 | '/new': 'newPosts',
5 |
6 | '/posts/:_id': {
7 | to: 'postPage',
8 | and: function(id) { Session.set('currentPostId', id); }
9 | },
10 |
11 | '/posts/:_id/edit': {
12 | to: 'postEdit',
13 | and: function(id) { Session.set('currentPostId', id); }
14 | },
15 |
16 | '/submit': 'postSubmit'
17 | });
18 |
19 | Meteor.Router.filters({
20 | 'requireLogin': function(page) {
21 | if (Meteor.user())
22 | return page;
23 | else if (Meteor.loggingIn())
24 | return 'loading';
25 | else
26 | return 'accessDenied';
27 | },
28 | 'clearErrors': function(page) {
29 | clearErrors();
30 | return page;
31 | }
32 | });
33 |
34 | Meteor.Router.filter('requireLogin', {only: 'postSubmit'});
35 | Meteor.Router.filter('clearErrors');
36 |
--------------------------------------------------------------------------------
/client/main.html:
--------------------------------------------------------------------------------
1 |
2 | Microscope
3 |
4 |
5 |
6 | {{> header}}
7 | {{> errors}}
8 |
9 | {{renderPage}}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/main.js:
--------------------------------------------------------------------------------
1 | newPostsHandle = Meteor.subscribeWithPagination('newPosts', 10);
2 | bestPostsHandle = Meteor.subscribeWithPagination('bestPosts', 10);
3 |
4 | Deps.autorun(function() {
5 | Meteor.subscribe('singlePost', Session.get('currentPostId'));
6 |
7 | Meteor.subscribe('comments', Session.get('currentPostId'));
8 | })
9 |
10 | Meteor.subscribe('notifications');
11 |
--------------------------------------------------------------------------------
/client/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | .grid-block, .main, .post, .comments li, .comment-form {
2 | background: #fff;
3 | -webkit-border-radius: 3px;
4 | -moz-border-radius: 3px;
5 | -ms-border-radius: 3px;
6 | -o-border-radius: 3px;
7 | border-radius: 3px;
8 | padding: 10px;
9 | margin-bottom: 10px;
10 | -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
11 | -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
12 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); }
13 |
14 | body {
15 | background: #eee;
16 | color: #666666; }
17 |
18 | .navbar {
19 | margin-bottom: 10px; }
20 | /* line 32, ../sass/style.scss */
21 | .navbar .navbar-inner {
22 | -webkit-border-radius: 0px 0px 3px 3px;
23 | -moz-border-radius: 0px 0px 3px 3px;
24 | -ms-border-radius: 0px 0px 3px 3px;
25 | -o-border-radius: 0px 0px 3px 3px;
26 | border-radius: 0px 0px 3px 3px; }
27 |
28 | #spinner {
29 | height: 300px; }
30 |
31 | .post {
32 | /* For modern browsers */
33 | /* For IE 6/7 (trigger hasLayout) */
34 | *zoom: 1;
35 | -webkit-transition: all 300ms 0ms;
36 | -webkit-transition-delay: ease-in;
37 | -moz-transition: all 300ms 0ms ease-in;
38 | -o-transition: all 300ms 0ms ease-in;
39 | transition: all 300ms 0ms ease-in;
40 | position: relative;
41 | opacity: 1; }
42 | .post:before, .post:after {
43 | content: "";
44 | display: table; }
45 | .post:after {
46 | clear: both; }
47 | .post.invisible {
48 | opacity: 0; }
49 | .post .upvote {
50 | display: block;
51 | margin: 7px 12px 0 0;
52 | float: left; }
53 | .post .post-content {
54 | float: left; }
55 | .post .post-content h3 {
56 | margin: 0;
57 | line-height: 1.4;
58 | font-size: 18px; }
59 | .post .post-content h3 a {
60 | display: inline-block;
61 | margin-right: 5px; }
62 | .post .post-content h3 span {
63 | font-weight: normal;
64 | font-size: 14px;
65 | display: inline-block;
66 | color: #aaaaaa; }
67 | .post .post-content p {
68 | margin: 0; }
69 | .post .discuss {
70 | display: block;
71 | float: right;
72 | margin-top: 7px; }
73 |
74 | .comments {
75 | list-style-type: none;
76 | margin: 0; }
77 | .comments li h4 {
78 | font-size: 16px;
79 | margin: 0; }
80 | .comments li h4 .date {
81 | font-size: 12px;
82 | font-weight: normal; }
83 | .comments li h4 a {
84 | font-size: 12px; }
85 | .comments li p:last-child {
86 | margin-bottom: 0; }
87 |
88 | .dropdown-menu span {
89 | display: block;
90 | padding: 3px 20px;
91 | clear: both;
92 | line-height: 20px;
93 | color: #bbb;
94 | white-space: nowrap; }
95 |
96 | .load-more {
97 | display: block;
98 | -webkit-border-radius: 3px;
99 | -moz-border-radius: 3px;
100 | -ms-border-radius: 3px;
101 | -o-border-radius: 3px;
102 | border-radius: 3px;
103 | background: rgba(0, 0, 0, 0.05);
104 | text-align: center;
105 | height: 60px;
106 | line-height: 60px;
107 | margin-bottom: 10px; }
108 | .load-more:hover {
109 | text-decoration: none;
110 | background: rgba(0, 0, 0, 0.1); }
111 |
--------------------------------------------------------------------------------
/client/views/comments/comment.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{author}}
5 | on {{submittedText}}
6 |
7 | {{body}}
8 |
9 |
--------------------------------------------------------------------------------
/client/views/comments/comment.js:
--------------------------------------------------------------------------------
1 | Template.comment.helpers({
2 | submittedText: function() {
3 | return new Date(this.submitted).toString();
4 | }
5 | });
--------------------------------------------------------------------------------
/client/views/comments/comment_submit.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/client/views/comments/comment_submit.js:
--------------------------------------------------------------------------------
1 | Template.commentSubmit.events({
2 | 'submit form': function(event, template) {
3 | event.preventDefault();
4 |
5 | var comment = {
6 | body: $(event.target).find('[name=body]').val(),
7 | postId: template.data._id
8 | };
9 |
10 | Meteor.call('comment', comment, function(error, commentId) {
11 | error && throwError(error.reason);
12 | });
13 | }
14 | });
--------------------------------------------------------------------------------
/client/views/includes/access_denied.html:
--------------------------------------------------------------------------------
1 |
2 | You can't get here! Please log in
3 |
4 |
--------------------------------------------------------------------------------
/client/views/includes/errors.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{#each errors}}
4 | {{> error}}
5 | {{/each}}
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{message}}
13 |
14 |
--------------------------------------------------------------------------------
/client/views/includes/errors.js:
--------------------------------------------------------------------------------
1 | Template.errors.helpers({
2 | errors: function() {
3 | return Errors.find();
4 | }
5 | });
6 |
7 | Template.error.rendered = function() {
8 | var error = this.data;
9 | Meteor.defer(function() {
10 | Errors.update(error._id, {$set: {seen: true}});
11 | });
12 | };
--------------------------------------------------------------------------------
/client/views/includes/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Microscope
5 |
6 | -
7 | New
8 |
9 | -
10 | Best
11 |
12 | {{#if currentUser}}
13 | -
14 | Submit Post
15 |
16 | {{/if}}
17 | -
18 | {{> notifications}}
19 |
20 |
21 |
22 | - {{loginButtons}}
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/client/views/includes/header.js:
--------------------------------------------------------------------------------
1 | Template.header.helpers({
2 | activeRouteClass: function(/* route names */) {
3 | var args = Array.prototype.slice.call(arguments, 0);
4 | args.pop();
5 |
6 | var active = _.any(args, function(name) {
7 | return location.pathname === Meteor.Router[name + 'Path']();
8 | });
9 |
10 | return active && 'active';
11 | }
12 | });
--------------------------------------------------------------------------------
/client/views/includes/loading.html:
--------------------------------------------------------------------------------
1 |
2 | {{> spinner}}
3 |
--------------------------------------------------------------------------------
/client/views/notifications/notifications.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Notifications
4 | {{#if notificationCount}}
5 | {{notificationCount}}
6 | {{/if}}
7 |
8 |
9 |
18 |
19 |
20 |
21 |
22 |
23 | {{commenterName}} commented on your post
24 |
25 |
26 |
--------------------------------------------------------------------------------
/client/views/notifications/notifications.js:
--------------------------------------------------------------------------------
1 | Template.notifications.helpers({
2 | notifications: function() {
3 | return Notifications.find({userId: Meteor.userId(), read: false});
4 | },
5 | notificationCount: function(){
6 | return Notifications.find({userId: Meteor.userId(), read: false}).count();
7 | }
8 | });
9 |
10 | Template.notification.events({
11 | 'click a': function() {
12 | Notifications.update(this._id, {$set: {read: true}});
13 | }
14 | })
--------------------------------------------------------------------------------
/client/views/posts/post_edit.html:
--------------------------------------------------------------------------------
1 |
2 | {{#with post}}
3 |
30 | {{/with}}
31 |
--------------------------------------------------------------------------------
/client/views/posts/post_edit.js:
--------------------------------------------------------------------------------
1 | Template.postEdit.helpers({
2 | post: function() {
3 | return Posts.findOne(Session.get('currentPostId'));
4 | }
5 | });
6 |
7 | Template.postEdit.events({
8 | 'submit form': function(e) {
9 | e.preventDefault();
10 |
11 | var currentPostId = Session.get('currentPostId');
12 |
13 | var postProperties = {
14 | url: $(e.target).find('[name=url]').val(),
15 | title: $(e.target).find('[name=title]').val()
16 | }
17 |
18 | Posts.update(currentPostId, {$set: postProperties}, function(error) {
19 | if (error) {
20 | // display the error to the user
21 | throwError(error.reason);
22 | } else {
23 | Meteor.Router.to('postPage', currentPostId);
24 | }
25 | });
26 | },
27 |
28 | 'click .delete': function(e) {
29 | e.preventDefault();
30 |
31 | if (confirm("Delete this post?")) {
32 | var currentPostId = Session.get('currentPostId');
33 | Posts.remove(currentPostId);
34 | Meteor.Router.to('postsList');
35 | }
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/client/views/posts/post_item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
⬆
4 |
5 |
6 |
7 | {{pluralize votes "Vote"}},
8 | submitted by {{author}},
9 | {{pluralize commentsCount "comment"}}
10 | {{#if ownPost}}Edit{{/if}}
11 |
12 |
13 |
Discuss
14 |
15 |
--------------------------------------------------------------------------------
/client/views/posts/post_item.js:
--------------------------------------------------------------------------------
1 | Template.postItem.helpers({
2 | ownPost: function() {
3 | return this.userId == Meteor.userId();
4 | },
5 | domain: function() {
6 | var a = document.createElement('a');
7 | a.href = this.url;
8 | return a.hostname;
9 | },
10 | upvotedClass: function() {
11 | var userId = Meteor.userId();
12 | if (userId && !_.include(this.upvoters, userId)) {
13 | return 'btn-primary upvoteable';
14 | } else {
15 | return 'disabled';
16 | }
17 | }
18 | });
19 |
20 | Template.postItem.rendered = function(){
21 | // animate post from previous position to new position
22 | var instance = this;
23 | var rank = instance.data._rank;
24 | var $this = $(this.firstNode);
25 | var postHeight = 80;
26 | var newPosition = rank * postHeight;
27 |
28 | // if element has a currentPosition (i.e. it's not the first ever render)
29 | if (typeof(instance.currentPosition) !== 'undefined') {
30 | var previousPosition = instance.currentPosition;
31 | // calculate difference between old position and new position and send element there
32 | var delta = previousPosition - newPosition;
33 | $this.css("top", delta + "px");
34 | } else {
35 | // it's the first ever render, so hide element
36 | $this.addClass("invisible");
37 | }
38 |
39 | // let it draw in the old position, then..
40 | Meteor.defer(function() {
41 | instance.currentPosition = newPosition;
42 | // bring element back to its new original position
43 | $this.css("top", "0px").removeClass("invisible");
44 | });
45 | };
46 |
47 | Template.postItem.events({
48 | 'click .upvoteable': function(event) {
49 | event.preventDefault();
50 | Meteor.call('upvote', this._id);
51 | }
52 | });
--------------------------------------------------------------------------------
/client/views/posts/post_page.html:
--------------------------------------------------------------------------------
1 |
2 | {{#with currentPost}}
3 | {{> postItem}}
4 |
5 |
10 |
11 | {{#if currentUser}}
12 | {{> commentSubmit}}
13 | {{else}}
14 | Please log in to leave a comment.
15 | {{/if}}
16 | {{/with}}
17 |
--------------------------------------------------------------------------------
/client/views/posts/post_page.js:
--------------------------------------------------------------------------------
1 | Template.postPage.helpers({
2 | currentPost: function() {
3 | return Posts.findOne(Session.get('currentPostId'));
4 | },
5 | comments: function() {
6 | return Comments.find({postId: this._id});
7 | }
8 | });
--------------------------------------------------------------------------------
/client/views/posts/post_submit.html:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
--------------------------------------------------------------------------------
/client/views/posts/post_submit.js:
--------------------------------------------------------------------------------
1 | Template.postSubmit.events({
2 | 'submit form': function(event) {
3 | event.preventDefault();
4 |
5 | var post = {
6 | url: $(event.target).find('[name=url]').val(),
7 | title: $(event.target).find('[name=title]').val(),
8 | message: $(event.target).find('[name=message]').val()
9 | }
10 |
11 | Meteor.call('post', post, function(error, id) {
12 | if (error) {
13 | // display the error to the user
14 | throwError(error.reason);
15 |
16 | // if the error is that the post already exists, take us there
17 | if (error.error === 302)
18 | Meteor.Router.to('postPage', error.details)
19 | } else {
20 | Meteor.Router.to('postPage', id);
21 | }
22 | });
23 | }
24 | });
--------------------------------------------------------------------------------
/client/views/posts/posts_list.html:
--------------------------------------------------------------------------------
1 |
2 | {{> postsList options}}
3 |
4 |
5 |
6 | {{> postsList options}}
7 |
8 |
9 |
10 |
11 | {{#each postsWithRank}}
12 | {{> postItem}}
13 | {{/each}}
14 |
15 | {{#if postsReady}}
16 | {{#unless allPostsLoaded}}
17 |
Load more
18 | {{/unless}}
19 | {{else}}
20 |
{{> spinner}}
21 | {{/if}}
22 |
23 |
24 |
--------------------------------------------------------------------------------
/client/views/posts/posts_list.js:
--------------------------------------------------------------------------------
1 | Template.newPosts.helpers({
2 | options: function() {
3 | return {
4 | sort: {submitted: -1},
5 | handle: newPostsHandle
6 | }
7 | }
8 | });
9 |
10 | Template.bestPosts.helpers({
11 | options: function() {
12 | return {
13 | sort: {votes: -1, submitted: -1},
14 | handle: bestPostsHandle
15 | }
16 | }
17 | });
18 |
19 | Template.postsList.helpers({
20 | postsWithRank: function() {
21 | var i = 0, options = {sort: this.sort, limit: this.handle.limit()};
22 | return Posts.find({}, options).map(function(post) {
23 | post._rank = i;
24 | i += 1;
25 | return post;
26 | });
27 | },
28 |
29 | postsReady: function() {
30 | return this.handle.ready();
31 | },
32 | allPostsLoaded: function() {
33 | return this.handle.ready() &&
34 | Posts.find().count() < this.handle.loaded();
35 | }
36 | });
37 |
38 | Template.postsList.events({
39 | 'click .load-more': function(event) {
40 | event.preventDefault();
41 | this.handle.loadNextPage();
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/collections/comments.js:
--------------------------------------------------------------------------------
1 | Comments = new Meteor.Collection('comments');
2 |
3 | Meteor.methods({
4 | comment: function(commentAttributes) {
5 | var user = Meteor.user();
6 | var post = Posts.findOne(commentAttributes.postId);
7 | // ensure the user is logged in
8 | if (!user)
9 | throw new Meteor.Error(401, "You need to login to make comments");
10 |
11 | if (!commentAttributes.body)
12 | throw new Meteor.Error(422, 'Please write some content');
13 |
14 | if (!commentAttributes.postId)
15 | throw new Meteor.Error(422, 'You must comment on a post');
16 |
17 | comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
18 | userId: user._id,
19 | author: user.username,
20 | submitted: new Date().getTime()
21 | });
22 |
23 | // update the post with the number of comments
24 | Posts.update(comment.postId, {$inc: {commentsCount: 1}});
25 |
26 | // create the comment, save the id
27 | comment._id = Comments.insert(comment);
28 |
29 | // now create a notification, informing the user that there's been a comment
30 | createCommentNotification(comment);
31 |
32 | return comment._id;
33 | }
34 | });
--------------------------------------------------------------------------------
/collections/notifications.js:
--------------------------------------------------------------------------------
1 | Notifications = new Meteor.Collection('notifications');
2 |
3 | Notifications.allow({
4 | update: ownsDocument
5 | });
6 |
7 | createCommentNotification = function(comment) {
8 | var post = Posts.findOne(comment.postId);
9 | Notifications.insert({
10 | userId: post.userId,
11 | postId: post._id,
12 | commentId: comment._id,
13 | commenterName: comment.author,
14 | read: false
15 | });
16 | };
--------------------------------------------------------------------------------
/collections/posts.js:
--------------------------------------------------------------------------------
1 | Posts = new Meteor.Collection('posts');
2 |
3 | Posts.allow({
4 | update: ownsDocument,
5 | remove: ownsDocument
6 | });
7 |
8 | Posts.deny({
9 | update: function(userId, post, fieldNames) {
10 | // may only edit the following three fields:
11 | return (_.without(fieldNames, 'url', 'title').length > 0);
12 | }
13 | });
14 |
15 | Meteor.methods({
16 | post: function(postAttributes) {
17 | var user = Meteor.user(),
18 | postWithSameLink = Posts.findOne({url: postAttributes.url});
19 |
20 | // ensure the user is logged in
21 | if (!user)
22 | throw new Meteor.Error(401, "You need to login to post new stories");
23 |
24 | // ensure the post has a title
25 | if (!postAttributes.title)
26 | throw new Meteor.Error(422, 'Please fill in a headline');
27 |
28 | // check that there are no previous posts with the same link
29 | if (postAttributes.url && postWithSameLink) {
30 | throw new Meteor.Error(302,
31 | 'This link has already been posted',
32 | postWithSameLink._id);
33 | }
34 |
35 | // pick out the whitelisted keys
36 | var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
37 | userId: user._id,
38 | author: user.username,
39 | submitted: new Date().getTime(),
40 | commentsCount: 0,
41 | upvoters: [], votes: 0
42 | });
43 |
44 | var postId = Posts.insert(post);
45 |
46 | return postId;
47 | },
48 |
49 | upvote: function(postId) {
50 | var user = Meteor.user();
51 | // ensure the user is logged in
52 | if (!user)
53 | throw new Meteor.Error(401, "You need to login to upvote");
54 |
55 | Posts.update({
56 | _id: postId,
57 | upvoters: {$ne: user._id}
58 | }, {
59 | $addToSet: {upvoters: user._id},
60 | $inc: {votes: 1}
61 | });
62 | }
63 | });
--------------------------------------------------------------------------------
/lib/permissions.js:
--------------------------------------------------------------------------------
1 | // check that the userId specified owns the documents
2 | ownsDocument = function(userId, doc) {
3 | return doc && doc.userId === userId;
4 | }
--------------------------------------------------------------------------------
/server/fixtures.js:
--------------------------------------------------------------------------------
1 | // Fixture data
2 | if (Posts.find().count() === 0) {
3 | var now = new Date().getTime();
4 |
5 | // create two users
6 | var tomId = Meteor.users.insert({
7 | profile: { name: 'Tom Coleman' }
8 | });
9 | var tom = Meteor.users.findOne(tomId);
10 | var sachaId = Meteor.users.insert({
11 | profile: { name: 'Sacha Greif' }
12 | });
13 | var sacha = Meteor.users.findOne(sachaId);
14 |
15 | var telescopeId = Posts.insert({
16 | title: 'Introducing Telescope',
17 | userId: sacha._id,
18 | author: sacha.profile.name,
19 | url: 'http://sachagreif.com/introducing-telescope/',
20 | submitted: now - 7 * 3600 * 1000,
21 | commentsCount: 2,
22 | upvoters: [], votes: 0
23 | });
24 |
25 | Comments.insert({
26 | postId: telescopeId,
27 | userId: tom._id,
28 | author: tom.profile.name,
29 | submitted: now - 5 * 3600 * 1000,
30 | body: 'Interesting project Sacha, can I get involved?'
31 | });
32 |
33 | Comments.insert({
34 | postId: telescopeId,
35 | userId: sacha._id,
36 | author: sacha.profile.name,
37 | submitted: now - 3 * 3600 * 1000,
38 | body: 'You sure can Tom!'
39 | });
40 |
41 | Posts.insert({
42 | title: 'Meteor',
43 | userId: tom._id,
44 | author: tom.profile.name,
45 | url: 'http://meteor.com',
46 | submitted: now - 10 * 3600 * 1000,
47 | commentsCount: 0,
48 | upvoters: [], votes: 0
49 | });
50 |
51 | Posts.insert({
52 | title: 'The Meteor Book',
53 | userId: tom._id,
54 | author: tom.profile.name,
55 | url: 'http://themeteorbook.com',
56 | submitted: now - 12 * 3600 * 1000,
57 | commentsCount: 0,
58 | upvoters: [], votes: 0
59 | });
60 |
61 | for (var i = 0; i < 10; i++) {
62 | Posts.insert({
63 | title: 'Test post #' + i,
64 | author: sacha.profile.name,
65 | userId: sacha._id,
66 | url: 'http://google.com/?q=test-' + i,
67 | submitted: now - i * 3600 * 1000,
68 | commentsCount: 0,
69 | upvoters: [], votes: 0
70 | });
71 | }
72 | }
--------------------------------------------------------------------------------
/server/publications.js:
--------------------------------------------------------------------------------
1 | Meteor.publish('newPosts', function(limit) {
2 | return Posts.find({}, {sort: {submitted: -1}, limit: limit});
3 | });
4 |
5 | Meteor.publish('bestPosts', function(limit) {
6 | return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
7 | });
8 |
9 | Meteor.publish('singlePost', function(id) {
10 | return id && Posts.find(id);
11 | });
12 |
13 |
14 | Meteor.publish('comments', function(postId) {
15 | return Comments.find({postId: postId});
16 | });
17 |
18 | Meteor.publish('notifications', function() {
19 | return Notifications.find({userId: this.userId});
20 | });
--------------------------------------------------------------------------------
/smart.json:
--------------------------------------------------------------------------------
1 | {
2 | "meteor": {
3 | "git": "https://github.com/meteor/meteor.git",
4 | "branch": "master"
5 | },
6 | "packages": {
7 | "router": {},
8 | "accounts-ui-bootstrap-dropdown": {},
9 | "spin": {},
10 | "paginated-subscription": {}
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/smart.lock:
--------------------------------------------------------------------------------
1 | {
2 | "meteor": {
3 | "git": "https://github.com/meteor/meteor.git",
4 | "branch": "master",
5 | "commit": "bfe4b72ebec4367eaddcdf0887fcf5e649d9dcb8"
6 | },
7 | "dependencies": {
8 | "basePackages": {
9 | "router": {},
10 | "accounts-ui-bootstrap-dropdown": {},
11 | "spin": {},
12 | "paginated-subscription": {}
13 | },
14 | "packages": {
15 | "router": {
16 | "git": "https://github.com/tmeasday/meteor-router.git",
17 | "tag": "v0.5.1",
18 | "commit": "32e377c7703bb119acccc859fc7296882903cbe7"
19 | },
20 | "accounts-ui-bootstrap-dropdown": {
21 | "git": "https://github.com/erobit/meteor-accounts-ui-bootstrap-dropdown.git",
22 | "tag": "v0.1.0",
23 | "commit": "e66558aaa4dc0d85c6ddb63bd2289b38c2b793f6"
24 | },
25 | "spin": {
26 | "git": "https://github.com/SachaG/meteor-spin.git",
27 | "tag": "v0.2.0",
28 | "commit": "fd3a2871a69442848b4b4e74f5e1ae28fa4a95a4"
29 | },
30 | "paginated-subscription": {
31 | "git": "https://github.com/tmeasday/meteor-paginated-subscription.git",
32 | "tag": "v0.1.1",
33 | "commit": "1ba670364ddac149f404158407b2afc89db42ce5"
34 | },
35 | "page-js-ie-support": {
36 | "git": "https://github.com/tmeasday/meteor-page-js-ie-support.git",
37 | "tag": "v1.3.5",
38 | "commit": "b99ed8380aefd10b2afc8f18d9eed4dd0d8ea9cb"
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------