diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..40b878d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders
new file mode 100755
index 0000000..aa60704
--- /dev/null
+++ b/.meteor/.finished-upgraders
@@ -0,0 +1,15 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
+1.4.0-remove-old-dev-bundle-link
+1.4.1-add-shell-server-package
diff --git a/.meteor/.gitignore b/.meteor/.gitignore
old mode 100644
new mode 100755
diff --git a/.meteor/.id b/.meteor/.id
new file mode 100755
index 0000000..82bc3eb
--- /dev/null
+++ b/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+1slbx1514ogf6j1s44x8f
diff --git a/.meteor/packages b/.meteor/packages
index 49f31b9..d40b418 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -1,15 +1,31 @@
# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
-standard-app-packages
-preserve-inputs
-bootstrap
-coffeescript
-router
-accounts-ui-bootstrap-dropdown
+meteor-base@1.0.4 # Packages every Meteor app needs to have
+mobile-experience@1.0.4 # Packages for a great mobile UX
+mongo@1.1.12 # The database Meteor supports right now
+blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
+reactive-var@1.0.10 # Reactive variable for tracker
+jquery@1.11.9 # Helpful client-side library
+tracker@1.1.0 # Meteor's client-side reactive programming library
+
+standard-minifier-css@1.2.0 # CSS minifier run for production mode
+standard-minifier-js@1.2.0 # JS minifier run for production mode
+es5-shim@4.6.14 # ECMAScript 5 compatibility for older browsers.
+ecmascript@0.5.8 # Enable ECMAScript2015+ syntax in app code
+shell-server@0.2.1 # Server-side component of the `meteor shell` command
+
+session
+iron:router
+sacha:spin
+stylus
accounts-password
-errors
-paginated-subscription
-spin
+twbs:bootstrap
+ian:accounts-ui-bootstrap-3
+check
+audit-argument-checks
+mquandalle:jade
+coffeescript
diff --git a/.meteor/platforms b/.meteor/platforms
new file mode 100755
index 0000000..efeba1b
--- /dev/null
+++ b/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/.meteor/release b/.meteor/release
old mode 100644
new mode 100755
index b6e6316..72980bc
--- a/.meteor/release
+++ b/.meteor/release
@@ -1 +1 @@
-0.7.0.1
+METEOR@1.4.1.1
diff --git a/.meteor/versions b/.meteor/versions
new file mode 100644
index 0000000..b7b4a06
--- /dev/null
+++ b/.meteor/versions
@@ -0,0 +1,98 @@
+accounts-base@1.2.11
+accounts-password@1.3.0
+allow-deny@1.0.5
+anti:i18n@0.4.3
+audit-argument-checks@1.0.7
+autoupdate@1.2.11
+babel-compiler@6.9.1
+babel-runtime@0.1.11
+base64@1.0.9
+binary-heap@1.0.9
+blaze@2.1.8
+blaze-html-templates@1.0.4
+blaze-tools@1.0.9
+boilerplate-generator@1.0.9
+caching-compiler@1.0.6
+caching-html-compiler@1.0.6
+callback-hook@1.0.9
+check@1.2.3
+coffeescript@1.1.4
+ddp@1.2.5
+ddp-client@1.2.9
+ddp-common@1.2.6
+ddp-rate-limiter@1.0.5
+ddp-server@1.2.10
+deps@1.0.12
+diff-sequence@1.0.6
+ecmascript@0.5.8
+ecmascript-runtime@0.3.14
+ejson@1.0.12
+email@1.1.17
+es5-shim@4.6.14
+fastclick@1.0.12
+geojson-utils@1.0.9
+hot-code-push@1.0.4
+html-tools@1.0.10
+htmljs@1.0.10
+http@1.1.8
+ian:accounts-ui-bootstrap-3@1.2.89
+id-map@1.0.8
+iron:controller@1.0.12
+iron:core@1.0.11
+iron:dynamic-template@1.0.12
+iron:layout@1.0.12
+iron:location@1.0.11
+iron:middleware-stack@1.1.0
+iron:router@1.0.13
+iron:url@1.0.11
+jquery@1.11.9
+launch-screen@1.0.12
+livedata@1.0.18
+localstorage@1.0.11
+logging@1.1.15
+meteor@1.2.17
+meteor-base@1.0.4
+minifier-css@1.2.14
+minifier-js@1.2.14
+minifiers@1.1.7
+minimongo@1.0.17
+mobile-experience@1.0.4
+mobile-status-bar@1.0.12
+modules@0.7.6
+modules-runtime@0.7.6
+mongo@1.1.12
+mongo-id@1.0.5
+mquandalle:jade@0.4.9
+mquandalle:jade-compiler@0.4.5
+npm-bcrypt@0.9.1
+npm-mongo@1.5.49
+observe-sequence@1.0.12
+ordered-dict@1.0.8
+promise@0.8.4
+random@1.0.10
+rate-limit@1.0.5
+reactive-dict@1.1.8
+reactive-var@1.0.10
+reload@1.1.10
+retry@1.0.8
+routepolicy@1.0.11
+sacha:spin@2.3.1
+service-configuration@1.0.10
+session@1.1.6
+sha@1.0.8
+shell-server@0.2.1
+spacebars@1.0.12
+spacebars-compiler@1.0.12
+srp@1.0.9
+standard-minifier-css@1.2.0
+standard-minifier-js@1.2.0
+stylus@2.512.5
+templating@1.1.14
+templating-tools@1.0.4
+tracker@1.1.0
+twbs:bootstrap@3.3.6
+ui@1.0.11
+underscore@1.0.9
+url@1.0.10
+webapp@1.3.11
+webapp-hashing@1.0.9
diff --git a/README.md b/README.md
index 30b9663..aea165a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Microscope (CoffeeScript)
-==============================
+# Microscope-Coffee-Jade
+The CoffeeScript version of Microscope.
-The official community-maintained [CoffeeScript](http://coffeescript.org/) version of [Microscope](https://github.com/DiscoverMeteor/Microscope), [Discover Meteor](https://www.discovermeteor.com/)'s' sample application.
+I make it compatible with the latest Meteor release (1.4.1.1)
diff --git a/client/helpers/config.coffee b/client/helpers/config.coffee
new file mode 100644
index 0000000..70c5563
--- /dev/null
+++ b/client/helpers/config.coffee
@@ -0,0 +1,2 @@
+Accounts.ui.config
+ passwordSignupFields: 'USERNAME_ONLY'
diff --git a/client/helpers/config.js.coffee b/client/helpers/config.js.coffee
deleted file mode 100644
index 421f3d1..0000000
--- a/client/helpers/config.js.coffee
+++ /dev/null
@@ -1,2 +0,0 @@
-Accounts.ui.config
- passwordSignupFields: 'USERNAME_ONLY'
diff --git a/client/helpers/error.coffee b/client/helpers/error.coffee
new file mode 100644
index 0000000..6862099
--- /dev/null
+++ b/client/helpers/error.coffee
@@ -0,0 +1,5 @@
+@Errors = new Mongo.Collection null
+
+throwError = (message) ->
+ Errors.insert
+ message: message
diff --git a/client/helpers/handlebars.coffee b/client/helpers/handlebars.coffee
new file mode 100644
index 0000000..20d79b0
--- /dev/null
+++ b/client/helpers/handlebars.coffee
@@ -0,0 +1,5 @@
+Template.registerHelper 'pluralize', (n, thing) ->
+ if n is 1
+ '1 ' + thing
+ else
+ n + ' ' + thing + 's'
diff --git a/client/helpers/handlebars.js.coffee b/client/helpers/handlebars.js.coffee
deleted file mode 100644
index 38a1926..0000000
--- a/client/helpers/handlebars.js.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-Handlebars.registerHelper 'pluralize', (n, thing) ->
- if n == 1
- "1 #{thing}"
- else
- "#{n} #{thing}s"
-
diff --git a/client/helpers/router.js.coffee b/client/helpers/router.js.coffee
deleted file mode 100644
index a81b9e7..0000000
--- a/client/helpers/router.js.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-Meteor.Router.add
- '/': {to: 'bestPosts', as: 'home'}
- '/best': to: 'bestPosts'
- '/new': 'newPosts'
- '/posts/:_id':
- to: 'postPage'
- and: (id) -> Session.set 'currentPostId', id
- '/posts/:_id/edit':
- to: 'postEdit'
- and: (id) -> Session.set 'currentPostId', id
- '/submit': 'postSubmit'
-
-Meteor.Router.filters
- 'requireLogin': (page) ->
- if Meteor.user()
- page
- else if Meteor.loggingIn()
- 'loading'
- else
- 'accessDenied'
- 'clearErrors': (page) ->
- Meteor.Errors.clear()
- page
-
-Meteor.Router.filter 'requireLogin', only: 'postSubmit'
-Meteor.Router.filter 'clearErrors'
diff --git a/client/main.css b/client/main.css
new file mode 100644
index 0000000..67206b7
--- /dev/null
+++ b/client/main.css
@@ -0,0 +1,160 @@
+.grid-block, .main, .post, .comments li, .comment-form {
+ background: #fff;
+ 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; }
+
+#main {
+ position: relative;
+}
+.page {
+ position: absolute;
+ top: 0px;
+ width: 100%;
+}
+
+.navbar {
+ margin-bottom: 10px; }
+ /* line 32, ../sass/style.scss */
+ .navbar .navbar-inner {
+ 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;
+ -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;
+ 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); }
+
+.posts .spinner-container{
+ position: relative;
+ height: 100px;
+}
+
+.jumbotron{
+ text-align: center;
+}
+.jumbotron h2{
+ font-size: 60px;
+ font-weight: 100;
+}
+
+@-webkit-keyframes fadeOut {
+ 0% {opacity: 0;}
+ 10% {opacity: 1;}
+ 90% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+@keyframes fadeOut {
+ 0% {opacity: 0;}
+ 10% {opacity: 1;}
+ 90% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+.errors{
+ position: fixed;
+ z-index: 10000;
+ padding: 10px;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ pointer-events: none;
+}
+.alert {
+ animation: fadeOut 2700ms ease-in 0s 1 forwards;
+ -webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards;
+ -moz-animation: fadeOut 2700ms ease-in 0s 1 forwards;
+ width: 250px;
+ float: right;
+ clear: both;
+ margin-bottom: 5px;
+ pointer-events: auto;
+}
diff --git a/client/main.html b/client/main.html
deleted file mode 100644
index 6c469d1..0000000
--- a/client/main.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
- Microscope
-
-
-
- {{> header}}
- {{> meteorErrors}}
-
- {{renderPage}}
-
-
-
diff --git a/client/main.jade b/client/main.jade
new file mode 100644
index 0000000..46745d0
--- /dev/null
+++ b/client/main.jade
@@ -0,0 +1,2 @@
+head
+ title Microscope
diff --git a/client/main.js b/client/main.js
new file mode 100644
index 0000000..e69de29
diff --git a/client/main.js.coffee b/client/main.js.coffee
deleted file mode 100644
index 86491fd..0000000
--- a/client/main.js.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-@newPostsHandle = Meteor.subscribeWithPagination 'newPosts', 10
-@topPostsHandle = Meteor.subscribeWithPagination 'newPosts', 10
-Meteor.autorun ->
- Meteor.subscribe 'singlePost', Session.get 'currentPostId'
- Meteor.subscribe 'comments', Session.get 'currentPostId'
-Meteor.subscribe 'notifications'
diff --git a/client/stylesheets/style.css b/client/stylesheets/style.css
deleted file mode 100644
index 3b94240..0000000
--- a/client/stylesheets/style.css
+++ /dev/null
@@ -1,52 +0,0 @@
-.grid-block, .main, .post, .comments li, .comment-form {
- background: #fff;
- border-radius: 3px;
- padding:10px;
- margin-bottom: 10px;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
-}
-body { background: #eee; color: #666666; }
-.navbar { margin-bottom: 10px }
-.navbar .navbar-inner { border-radius: 0px 0px 3px 3px; }
-#spinner { height: 300px }
-.post {
- *zoom: 1;
- -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;
- position: relative;
- opacity: 1;
-}
-.post:before, .post:after { content: ""; display: table; }
-.post:after { clear: both } .post.invisible { opacity: 0 }
-.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;
- 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); }
diff --git a/client/templates/application/layout.coffee b/client/templates/application/layout.coffee
new file mode 100644
index 0000000..bedbec1
--- /dev/null
+++ b/client/templates/application/layout.coffee
@@ -0,0 +1,10 @@
+Template.layout.onRendered ->
+ this.find('#main')._uihooks =
+ insertElement: (node, next) ->
+ $(node)
+ .hide()
+ .insertBefore(next)
+ .fadeIn()
+ removeElement: (node) ->
+ $(node).fadeOut ->
+ $(this).remove()
diff --git a/client/templates/application/layout.html b/client/templates/application/layout.html
new file mode 100644
index 0000000..ea30944
--- /dev/null
+++ b/client/templates/application/layout.html
@@ -0,0 +1,9 @@
+
+
+ {{> header}}
+ {{> errors}}
+
+ {{> yield}}
+
+
+
diff --git a/client/templates/application/notFound.tpl.jade b/client/templates/application/notFound.tpl.jade
new file mode 100644
index 0000000..2e74fec
--- /dev/null
+++ b/client/templates/application/notFound.tpl.jade
@@ -0,0 +1,3 @@
+div.not-found.page.jumbotron
+ h2 404
+ p Sorry, we couldn't find a page at this address
diff --git a/client/templates/comments/commentItem.coffee b/client/templates/comments/commentItem.coffee
new file mode 100644
index 0000000..9ed5ce0
--- /dev/null
+++ b/client/templates/comments/commentItem.coffee
@@ -0,0 +1,3 @@
+Template.commentItem.helpers
+ submittedText: ->
+ this.submitted.toString()
diff --git a/client/templates/comments/commentItem.tpl.jade b/client/templates/comments/commentItem.tpl.jade
new file mode 100644
index 0000000..714e2e4
--- /dev/null
+++ b/client/templates/comments/commentItem.tpl.jade
@@ -0,0 +1,5 @@
+li
+ h4
+ span.author {{author}}
+ span.date on {{submittedText}}
+ p {{body}}
diff --git a/client/templates/comments/commentSubmit.coffee b/client/templates/comments/commentSubmit.coffee
new file mode 100644
index 0000000..33f4a0a
--- /dev/null
+++ b/client/templates/comments/commentSubmit.coffee
@@ -0,0 +1,25 @@
+Template.commentSubmit.onCreated ->
+ Session.set 'commentSubmitErrors', {}
+
+Template.commentSubmit.helpers
+ errorMessage: (field) ->
+ Session.get('commentSubmitErrors')[field]
+ errorClass: (field) ->
+ !! Session.get('commentSubmitErrors')[field] ? "has-error" : ''
+
+Template.commentSubmit.events
+ 'submit form': (e, template) ->
+ e.preventDefault()
+ $body = $(e.target).find('[name=body]')
+ comment =
+ body: $body.val()
+ postId: template.data._id
+ errors = {}
+ if not comment.body
+ errors.body = "Please write some"
+ Session.set 'commentSubmitErrors', errors
+ Meteor.call 'commentInsert', comment, (error, commentId) ->
+ if error
+ throwError error.reason
+ else
+ $body.val ''
diff --git a/client/templates/comments/commentSubmit.tpl.jade b/client/templates/comments/commentSubmit.tpl.jade
new file mode 100644
index 0000000..a00756f
--- /dev/null
+++ b/client/templates/comments/commentSubmit.tpl.jade
@@ -0,0 +1,7 @@
+form.comment-form.form(name="comment")
+ div.form-group(class="{{errorClass 'body'}}")
+ div.controls
+ label(for="body") Comment on this post
+ textarea.form-control#body(name="body", rows="3")
+ span.help-block {{errorMessage 'body'}}
+ button.btn.btn-primary(type="submit") Add Comment
diff --git a/client/templates/includes/accessDenied.tpl.jade b/client/templates/includes/accessDenied.tpl.jade
new file mode 100644
index 0000000..e8d4102
--- /dev/null
+++ b/client/templates/includes/accessDenied.tpl.jade
@@ -0,0 +1,3 @@
+div.access-denied.page.jumbotron
+ h2 Access Denied
+ p You can't get here! Please log in.
diff --git a/client/templates/includes/error.tpl.jade b/client/templates/includes/error.tpl.jade
new file mode 100644
index 0000000..f18be7f
--- /dev/null
+++ b/client/templates/includes/error.tpl.jade
@@ -0,0 +1,3 @@
+div.alert.alert-danger(role='alert')
+ button.close(type="button", data-dismiss="alert") ×
+ {{message}}
diff --git a/client/templates/includes/errors.coffee b/client/templates/includes/errors.coffee
new file mode 100644
index 0000000..e9171ae
--- /dev/null
+++ b/client/templates/includes/errors.coffee
@@ -0,0 +1,9 @@
+Template.errors.helpers
+ errors: ->
+ Errors.find()
+
+Template.error.onRendered ->
+ error = this.data
+ Meteor.setTimeout ->
+ Errors.remove error._id
+ 3000
diff --git a/client/templates/includes/errors.tpl.jade b/client/templates/includes/errors.tpl.jade
new file mode 100644
index 0000000..7ff031a
--- /dev/null
+++ b/client/templates/includes/errors.tpl.jade
@@ -0,0 +1 @@
+div.errors {{#each errors}} {{> error}} {{/each}}
diff --git a/client/templates/includes/header.coffee b/client/templates/includes/header.coffee
new file mode 100644
index 0000000..2e46c59
--- /dev/null
+++ b/client/templates/includes/header.coffee
@@ -0,0 +1,8 @@
+Template.header.helpers
+ activeRouteClass: ->
+ args = Array.prototype.slice.call arguments, 0
+ args.pop()
+
+ active = _.any args (name) ->
+ Router.current() and Router.current().route.getName() is name
+ active and 'active'
diff --git a/client/templates/includes/header.tpl.jade b/client/templates/includes/header.tpl.jade
new file mode 100644
index 0000000..48991c7
--- /dev/null
+++ b/client/templates/includes/header.tpl.jade
@@ -0,0 +1,21 @@
+nav.navbar.navbar-default(role="navigation")
+ div.navbar-header
+ button.navbar-toggle.collapsed(type="button")
+ span.sr-only Toggle navigation
+ span.icon-bar
+ span.icon-bar
+ span.icon-bar
+ a.navbar-brand(href="{{pathFor 'home'}}") Microscope
+ div.collapse.navbar-collapse#navigation
+ ul.nav.navbar-nav
+ li(class="{{activeRouteClass 'home' 'newPosts'}}")
+ a(href="{{pathFor 'newPosts'}}") New
+ li(class="{{activeRouteClass 'bestPosts'}}")
+ a(href="{{pathFor 'bestPosts'}}") Best
+ if currentUser
+ li(class="{{activeRouteClass 'postSubmit'}}")
+ a(href="{{pathFor 'postSubmit'}}") Submit Post
+ li.dropdown
+ +notifications
+ ul.nav.navbar-nav.navbar-right
+ +loginButtons
diff --git a/client/templates/includes/loading.tpl.jade b/client/templates/includes/loading.tpl.jade
new file mode 100644
index 0000000..67df502
--- /dev/null
+++ b/client/templates/includes/loading.tpl.jade
@@ -0,0 +1 @@
++spinner
diff --git a/client/templates/notifications/notificationItem.tpl.jade b/client/templates/notifications/notificationItem.tpl.jade
new file mode 100644
index 0000000..c31abb7
--- /dev/null
+++ b/client/templates/notifications/notificationItem.tpl.jade
@@ -0,0 +1,4 @@
+li
+ a(href="{{notificationPostPath}}")
+ strong {{commenterName}}
+ commented on your post
diff --git a/client/templates/notifications/notifications.coffee b/client/templates/notifications/notifications.coffee
new file mode 100644
index 0000000..5e0db23
--- /dev/null
+++ b/client/templates/notifications/notifications.coffee
@@ -0,0 +1,22 @@
+Template.notifications.helpers
+ notifications: ->
+ Notifications.find
+ userId: Meteor.userId()
+ read: false
+ notificationCount: ->
+ selector =
+ userId: Meteor.userId()
+ read: false
+ counter = Notifications.find selector
+ counter.count()
+
+Template.notificationItem.helpers
+ notificationPostPath: ->
+ Router.routes.postPage.path
+ _id: this.postId
+
+Template.notificationItem.events
+ 'click a': ->
+ Notifications.update this._id,
+ $set:
+ read: true
diff --git a/client/templates/notifications/notifications.tpl.jade b/client/templates/notifications/notifications.tpl.jade
new file mode 100644
index 0000000..ef493f3
--- /dev/null
+++ b/client/templates/notifications/notifications.tpl.jade
@@ -0,0 +1,16 @@
+a.dropdown-toggle(href="#", data-toggle="dropdown") Notifications
+ if notificationCount
+ span.badge.badge-inverse {{notificationCount}}
+ b.caret
+
+ul.notification.dropdown-menu
+ if notificationCount
+ {{#each notifications}} {{> notificationItem}} {{/each}}
+ else
+ li
+ span No notifications
+
+
+// {{#if notificationCount}} {{notificationCount}} {{/if}}
+// {{#if notificationCount}} {{#each notifications}} {{> notificationItem}} {{/each}} {{else}} No notifications {{/if}}
+
diff --git a/client/templates/posts/postEdit.coffee b/client/templates/posts/postEdit.coffee
new file mode 100644
index 0000000..5c3962f
--- /dev/null
+++ b/client/templates/posts/postEdit.coffee
@@ -0,0 +1,33 @@
+Template.postEdit.onCreated ->
+ Session.set 'postEditErrors', {}
+
+Template.postEdit.helpers
+ errorMessage: (field) ->
+ Session.get('postEditErrors')[field]
+ errorClass: (field) ->
+ !! Session.get('postEditErrors')[field] ? 'has-error' : ''
+
+Template.postEdit.events
+ 'submit form': (e) ->
+ e.preventDefault()
+ currentPostId = this._id
+ postProperties =
+ url: $(e.target).find('[name=url]').val()
+ title: $(e.target).find('[name=title]').val()
+ errors = validatePost postProperties
+ if errors.title or errors.url
+ Session.set 'postEditErrors', errors
+ Posts.update currentPostId,
+ $set: postProperties,
+ (error) ->
+ if error
+ throwError error.reason
+ else
+ Router.go 'postPage',
+ _id: currentPostId
+ 'click .delete': (e) ->
+ e.preventDefault()
+ if confirm "Delete this Post?"
+ currentPostId = this._id
+ Posts.remove currentPostId
+ Router.go 'home'
diff --git a/client/templates/posts/postEdit.tpl.jade b/client/templates/posts/postEdit.tpl.jade
new file mode 100644
index 0000000..e1d3d8c
--- /dev/null
+++ b/client/templates/posts/postEdit.tpl.jade
@@ -0,0 +1,16 @@
+form.main.form.page
+
+ div.form-group(class='{{errorClass "url"}}')
+ label.control-label(for='url') URL
+ div.controls
+ input.form-control#url(name='url', type='text', value='{{url}}', placeholder='Your URL')
+ span.help-block {{errorMessage 'url'}}
+
+ div.form-group(class='{{errorClass "title"}}')
+ label.control-label(for='title') Title
+ div.controls
+ input.form-control#title(name='title', type='text', value='{{title}}', placeholder='Your Title')
+ span.help-block {{errorMessage 'title'}}
+
+ input.btn.btn-primary.submit(type='submit', value='Submit')
+ a.btn.btn-danger.delete(href='#') Delete post
diff --git a/client/templates/posts/postItem.coffee b/client/templates/posts/postItem.coffee
new file mode 100644
index 0000000..88ee52a
--- /dev/null
+++ b/client/templates/posts/postItem.coffee
@@ -0,0 +1,21 @@
+Template.postItem.helpers
+
+ ownPost: ->
+ this.userId is Meteor.userId()
+
+ domain: ->
+ a = document.createElement 'a'
+ a.href = this.url
+ a.hostname
+
+ upvotedClass: ->
+ userId = Meteor.userId()
+ if userId and not _.include this.upvoters, userId
+ 'btn-primary upvotable'
+ else
+ 'disabled'
+
+Template.postItem.events
+ 'click .upvotable': (e) ->
+ e.preventDefault()
+ Meteor.call 'upvote', this._id
diff --git a/client/templates/posts/postItem.tpl.jade b/client/templates/posts/postItem.tpl.jade
new file mode 100644
index 0000000..d28ae4e
--- /dev/null
+++ b/client/templates/posts/postItem.tpl.jade
@@ -0,0 +1,12 @@
+div.post
+ a.upvote.btn.btn-default(class="#{upvotedClass}", href="#") Up
+ div.post-content
+ h3
+ a(href='#{url}') #{title}
+ span #{domain}
+ p {{pluralize votes 'Vote'}}, submitted by #{author}
+ a(href="{{pathFor 'postPage'}}") {{pluralize commentsCount 'comment'}}
+ if ownPost
+ a(href="{{pathFor 'postEdit'}}") Edit
+ a.discuss.btn.btn-default(href="{{pathFor 'postPage'}}") Discuss
+
diff --git a/client/templates/posts/postPage.coffee b/client/templates/posts/postPage.coffee
new file mode 100644
index 0000000..1b7b20a
--- /dev/null
+++ b/client/templates/posts/postPage.coffee
@@ -0,0 +1,4 @@
+Template.postPage.helpers
+ comments: ->
+ Comments.find
+ postId: this._id
diff --git a/client/templates/posts/postPage.tpl.jade b/client/templates/posts/postPage.tpl.jade
new file mode 100644
index 0000000..79ccc98
--- /dev/null
+++ b/client/templates/posts/postPage.tpl.jade
@@ -0,0 +1,9 @@
+
+div.post-page.page
+ +postItem
+ ul.comments
+ {{#each comments}} {{> commentItem}} {{/each}}
+ if currentUser
+ +commentSubmit
+ else
+ p Please log in to leave a comment.
diff --git a/client/templates/posts/postSubmit.coffee b/client/templates/posts/postSubmit.coffee
new file mode 100644
index 0000000..4588af4
--- /dev/null
+++ b/client/templates/posts/postSubmit.coffee
@@ -0,0 +1,25 @@
+Template.postSubmit.onCreated ->
+ Session.set 'postSubmitErrors', {}
+
+Template.postSubmit.helpers
+ errorMessage: (field) ->
+ Session.get('postSubmitErrors')[field]
+ errorClass: (field) ->
+ !! Session.get('postSubmitErrors')[field] ? 'has-error' : ''
+
+Template.postSubmit.events
+ 'submit form': (e) ->
+ e.preventDefault()
+ post =
+ url: $(e.target).find('[name=url]').val()
+ title: $(e.target).find('[name=title]').val()
+ errors = validatePost post
+ if errors.title or errors.url
+ Session.set 'postSubmitErrors', errors
+ Meteor.call 'postInsert', post, (error, result) ->
+ if error
+ throwError error.reason
+ if result.postExists
+ throwError 'This link has already been posted'
+ Router.go 'postPage',
+ _id: result._id
diff --git a/client/templates/posts/postSubmit.tpl.jade b/client/templates/posts/postSubmit.tpl.jade
new file mode 100644
index 0000000..acb8996
--- /dev/null
+++ b/client/templates/posts/postSubmit.tpl.jade
@@ -0,0 +1,14 @@
+form.main.form.page
+
+ div.form-group(class="{{errorClass 'url'}}")
+ label.control-label(for="url") URL
+ div.controls
+ input.form-control#url(name="url", type="text", value="", placeholder="Your URL")
+ span.help-block {{errorMessage 'url'}}
+
+ div.form-group(class="{{errorClass 'title'}}")
+ label.control-label(for='title') Title
+ div.controls
+ input.form-control#title(name="title", type="text", value="", placeholder="Your Title")
+
+ input.btn.btn-primary(type="submit", value="Submit")
diff --git a/client/templates/posts/postsList.coffee b/client/templates/posts/postsList.coffee
new file mode 100644
index 0000000..65fb5ce
--- /dev/null
+++ b/client/templates/posts/postsList.coffee
@@ -0,0 +1,31 @@
+Template.postsList.onRendered ->
+ this.find('.wrapper')._uihooks =
+ insertElement: (node, next) ->
+ $(node)
+ .hide()
+ .insertBefore next
+ .fadeIn()
+
+ moveElement: (node, next) ->
+ $node = $(node)
+ $next = $(next)
+ oldTop = $node.offset().top
+ height = $(node).outerHeight true
+ $inBetween = $(next).nextUntil node
+ if $inBetween.length is 0
+ $inBetween = $(node).nextUntil next
+ $(node).insertBefore next
+ newTop = $(node).offset().top
+ $(node)
+ .removeClass 'animate'
+ .css('top', oldTop - newTop)
+ $inBetween
+ .removeClass 'animate'
+ .css('top', oldTop < newTop ? height : -1 * height)
+ $(node).offset()
+ $(node).addClass('animate').css('top', 0)
+ $inBetween.addClass('animate').css('top', 0)
+
+ removeElement: (node) ->
+ $(node).fadeOut ->
+ $(this).remove()
diff --git a/client/templates/posts/postsList.tpl.jade b/client/templates/posts/postsList.tpl.jade
new file mode 100644
index 0000000..5720baf
--- /dev/null
+++ b/client/templates/posts/postsList.tpl.jade
@@ -0,0 +1,9 @@
+
+div.posts.page
+ div.wrapper
+ {{#each posts}} {{> postItem}} {{/each}}
+ if nextPath
+ a.load-more(href="{{nextPath}}") Load more
+ else
+ unless ready
+ +spinner
diff --git a/client/views/comments/comment.html b/client/views/comments/comment.html
deleted file mode 100644
index e880da2..0000000
--- a/client/views/comments/comment.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- {{author}}
- {{submittedText}}
-
- {{body}}
-
-
diff --git a/client/views/comments/comment.js.coffee b/client/views/comments/comment.js.coffee
deleted file mode 100644
index e7160ac..0000000
--- a/client/views/comments/comment.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-Template.comment.helpers
- submittedText: ->
- new Date(this.submitted).toString()
diff --git a/client/views/comments/comment_submit.html b/client/views/comments/comment_submit.html
deleted file mode 100644
index b7263c1..0000000
--- a/client/views/comments/comment_submit.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
diff --git a/client/views/comments/comment_submit.js.coffee b/client/views/comments/comment_submit.js.coffee
deleted file mode 100644
index 90398b2..0000000
--- a/client/views/comments/comment_submit.js.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-Template.commentSubmit.events
- 'submit form': (event, template) ->
- event.preventDefault()
-
- comment =
- body: $(event.target).find('[name=body]').val()
- postId: template.data._id
-
- Meteor.call 'comment', comment, (error, commentId) ->
- error && throwError error.reason
diff --git a/client/views/includes/access_denied.html b/client/views/includes/access_denied.html
deleted file mode 100644
index 1794b37..0000000
--- a/client/views/includes/access_denied.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
- You can't go there! Please login.
-
diff --git a/client/views/includes/header.html b/client/views/includes/header.html
deleted file mode 100644
index 7b54a71..0000000
--- a/client/views/includes/header.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
diff --git a/client/views/includes/header.js.coffee b/client/views/includes/header.js.coffee
deleted file mode 100644
index 983e74c..0000000
--- a/client/views/includes/header.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-Template.header.helpers
- activeRouteClass: ->
- args = Array.prototype.slice.call arguments, 0
- args.pop()
-
- active = _.any args, (name) ->
- location.pathname == Meteor.Router[name + "Path"]()
-
- active && 'active'
diff --git a/client/views/includes/loading.html b/client/views/includes/loading.html
deleted file mode 100644
index 23fb015..0000000
--- a/client/views/includes/loading.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
- {{>spinner}}
-
diff --git a/client/views/notifications/notifications.html b/client/views/notifications/notifications.html
deleted file mode 100644
index 4663d18..0000000
--- a/client/views/notifications/notifications.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
- Notifications
- {{#if notificationCount}}
- {{notificationCount}}
- {{/if}}
-
-
-
-
-
-
-
-
- {{commenterName}} commented on your post
-
-
-
diff --git a/client/views/notifications/notifications.js.coffee b/client/views/notifications/notifications.js.coffee
deleted file mode 100644
index 580968a..0000000
--- a/client/views/notifications/notifications.js.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-Template.notifications.helpers
- notifications: ->
- Notifications.find userId: Meteor.userId(), read: false
-
- notificationCount: =>
- Notifications.find(userId: Meteor.userId(), read: false).count()
-
-Template.notification.events
- 'click a': ->
- Notifications.update this._id, {$set: {read: true}}
diff --git a/client/views/posts/post_edit.html b/client/views/posts/post_edit.html
deleted file mode 100644
index 59548c3..0000000
--- a/client/views/posts/post_edit.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
- {{#with post}}
-
- {{/with}}
-
diff --git a/client/views/posts/post_edit.js.coffee b/client/views/posts/post_edit.js.coffee
deleted file mode 100644
index c43e2f3..0000000
--- a/client/views/posts/post_edit.js.coffee
+++ /dev/null
@@ -1,27 +0,0 @@
-Template.postEdit.helpers
- post: ->
- Posts.findOne Session.get 'currentPostId'
-
-Template.postEdit.events
- 'submit form': (e) ->
- e.preventDefault()
-
- currentPostId = Session.get 'currentPostId'
-
- postProperties =
- url: $(e.target).find('[name=url]').val()
- title: $(e.target).find('[name=title]').val()
-
- Posts.update currentPostId, {$set: postProperties}, (error) ->
- if error
- Meteor.Errors.throw error.reason
- else
- Meteor.Router.to 'postPage', currentPostId
-
- 'click .delete': (e) ->
- e.preventDefault()
-
- if confirm 'Delete this post?'
- currentPostId = Session.get 'currentPostId'
- Posts.remove currentPostId
- Meteor.Router.to 'postsList'
diff --git a/client/views/posts/post_item.html b/client/views/posts/post_item.html
deleted file mode 100644
index 09dcd88..0000000
--- a/client/views/posts/post_item.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
⬆
-
-
-
- {{pluralize votes "Vote"}},
- submitted by {{author}}
- {{commentsCount}} comments
- {{# if ownPost}}Edit{{/if}}
-
-
-
Discuss
-
-
diff --git a/client/views/posts/post_item.js.coffee b/client/views/posts/post_item.js.coffee
deleted file mode 100644
index 4a31ce9..0000000
--- a/client/views/posts/post_item.js.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-Template.postItem.helpers
- ownPost: ->
- this.userId == Meteor.userId()
-
- domain: ->
- a = document.createElement('a')
- a.href = this.url
- a.hostname
-
- upvotedClass: ->
- userId = Meteor.userId()
- if userId && !_.include this.upvoters, userId
- 'btn-primary upvoteable'
- else
- 'disabled'
-
-Template.postItem.events
- 'click .upvoteable': (event) ->
- event.preventDefault()
- Meteor.call 'upvote', this._id
-
-Template.postItem.rendered = ->
- instance = this
- rank = instance.data._rank
- $this = $(this.firstNode)
- postHeight = 80
- newPosition = rank * postHeight
-
- if instance.currentPosition?
- previousPosition = instance.currentPosition
- delta = previousPosition - newPosition
- $this.css 'top', "#{delta}px"
- else
- $this.addClass 'invisible'
-
- Meteor.defer ->
- instance.currentPosition = newPosition
- $this.css('top', '0px').removeClass 'invisible'
diff --git a/client/views/posts/post_page.html b/client/views/posts/post_page.html
deleted file mode 100644
index 993612f..0000000
--- a/client/views/posts/post_page.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
- {{#with currentPost}}
- {{> postItem}}
-
-
-
- {{#if currentUser}}
- {{> commentSubmit}}
- {{/if}}
- {{/with}}
-
diff --git a/client/views/posts/post_page.js.coffee b/client/views/posts/post_page.js.coffee
deleted file mode 100644
index f106efe..0000000
--- a/client/views/posts/post_page.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-Template.postPage.helpers
- currentPost: ->
- Posts.findOne Session.get('currentPostId')
- comments: ->
- Comments.find postId: this._id
diff --git a/client/views/posts/post_submit.html b/client/views/posts/post_submit.html
deleted file mode 100644
index 22463d6..0000000
--- a/client/views/posts/post_submit.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
diff --git a/client/views/posts/post_submit.js.coffee b/client/views/posts/post_submit.js.coffee
deleted file mode 100644
index 42f91c7..0000000
--- a/client/views/posts/post_submit.js.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-Template.postSubmit.events
- 'submit form': (event) ->
- event.preventDefault()
-
- post =
- url: $(event.target).find('[name=url]').val()
- title: $(event.target).find('[name=title]').val()
- message: $(event.target).find('[name=message]').val()
-
- Meteor.call 'post', post, (error, id) ->
- if error
- Meteor.Errors.throw error.reason
-
- if error.error == 302
- Meteor.Router.to 'postPage', error.details
- else
- Meteor.Router.to 'postPage', id
diff --git a/client/views/posts/posts_list.html b/client/views/posts/posts_list.html
deleted file mode 100644
index f3ffdd1..0000000
--- a/client/views/posts/posts_list.html
+++ /dev/null
@@ -1,23 +0,0 @@
-
- {{> postsList options}}
-
-
-
- {{> postsList options}}
-
-
-
-
- {{#each postsWithRank}}
- {{> postItem}}
- {{/each}}
-
- {{#if postsReady}}
- {{#unless allPostsLoaded}}
-
Load More
- {{/unless}}
- {{else}}
-
{{> spinner}}
- {{/if}}
-
-
diff --git a/client/views/posts/posts_list.js.coffee b/client/views/posts/posts_list.js.coffee
deleted file mode 100644
index adcbfea..0000000
--- a/client/views/posts/posts_list.js.coffee
+++ /dev/null
@@ -1,31 +0,0 @@
-Template.newPosts.helpers
- options: ->
- sort: {submitted: -1}
- handle: newPostsHandle
-
-Template.bestPosts.helpers
- options: ->
- sort: {votes: -1, submitted: -1}
- handle: topPostsHandle
-
-Template.postsList.helpers
- postsWithRank: ->
- i = 0
- options =
- sort: this.sort
- limit: this.handle.limit()
- Posts.find({}, options).map (post) ->
- post._rank = i
- i += 1
- post
-
- postsReady: ->
- !this.handle.loading()
-
- allPostsLoaded: ->
- !this.handle.loading() && Posts.find().count() < this.handle.loaded()
-
-Template.postsList.events
- 'click .load-more': (event) ->
- event.preventDefault()
- this.handle.loadNextPage()
diff --git a/collections/comments.js.coffee b/collections/comments.js.coffee
deleted file mode 100644
index f439bbf..0000000
--- a/collections/comments.js.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-@Comments = new Meteor.Collection 'comments'
-
-Meteor.methods
- comment: (commentAttributes) ->
- user = Meteor.user()
- post = Posts.findOne commentAttributes.postId
-
- if !user
- throw new Meteor.ERror 401, 'You need to login to create comments'
-
- if !commentAttributes.body
- throw new Meteor.Error 422, 'Please write something'
-
- if !commentAttributes.postId
- throw new Meteor.Error 422, 'You must comment on an existing post'
-
- comment = _.extend _.pick(commentAttributes, 'postId', 'body'),
- userId: user._id
- author: user.username
- submitted: new Date().getTime()
-
- Posts.update comment.postId, {$inc: {commentsCount: 1}}
- comment._id = Comments.insert comment
- createCommentNotification comment
- comment._id
diff --git a/collections/notifications.js.coffee b/collections/notifications.js.coffee
deleted file mode 100644
index 8b3195a..0000000
--- a/collections/notifications.js.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-@Notifications = new Meteor.Collection 'notifications'
-
-Notifications.allow
- update: ownsDocument
-
-@createCommentNotification = (comment) ->
- post = Posts.findOne comment.postId
- Notifications.insert
- userId: post.userId
- postId: post._id
- commentId: comment._id
- commenterName: comment.author
- read: false
diff --git a/collections/posts.js.coffee b/collections/posts.js.coffee
deleted file mode 100644
index a92cfa7..0000000
--- a/collections/posts.js.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-@Posts = new Meteor.Collection 'posts'
-
-Posts.allow
- update: ownsDocument
- remove: ownsDocument
-
-Posts.deny
- update: (userId, post, fieldNames) ->
- _.without(fieldNames, 'url', 'title').length > 0
-
-Meteor.methods
- post: (postAttributes) ->
- user = Meteor.user()
- postWithSameLink = Posts.findOne url: postAttributes.url
-
- if !user
- throw new Meteor.Error 401, 'You need to login to post new stories'
-
- if !postAttributes.title
- throw new Meteor.Error 422, 'Please fill in a headline'
-
- if postAttributes.url && postWithSameLink
- throw new Meteor.Error 302, 'This link has already been posted', postWithSameLink._id
-
- post = _.extend _.pick(postAttributes, 'url', 'message'),
- title: "#{postAttributes.title} #{if this.isSimulation then ' (client)' else ' (server)'}"
- userId: user._id
- author: user.username
- submitted: new Date().getTime()
- commentsCount: 0
- upvoters: []
- votes: 0
-
- Posts.insert post
-
- upvote: (postId) ->
- user = Meteor.user()
- if !user
- throw new Meteor.Error 401, 'You need to login to cast votes'
- post = Posts.findOne postId
- if !post
- throw new Meteor.Error 422, 'Post not found'
- if _.include post.upvoters, user._id
- throw new Meteor.Error 422, "You've already voted for this post"
-
- Posts.update {
- _id: postId,
- upvoters: {$ne: user._id}
- }, {
- $addToSet: {upvoters: user._id}
- $inc: {votes: 1}
- }
diff --git a/lib/collections/comments.coffee b/lib/collections/comments.coffee
new file mode 100644
index 0000000..f145631
--- /dev/null
+++ b/lib/collections/comments.coffee
@@ -0,0 +1,28 @@
+@Comments = new Mongo.Collection 'comments'
+
+Meteor.methods
+ commentInsert: (commentAttributes) ->
+ check this.userId, String
+ check commentAttributes,
+ postId: String,
+ body: String
+
+ user = Meteor.user()
+ post = Posts.findOne commentAttributes.postId
+
+ if not post
+ throw new Meteor.Error 'invalid-comment', 'You must comment'
+ comment = _.extend commentAttributes,
+ userId: user._id,
+ author: user.username,
+ submitted: new Date()
+
+ Posts.update comment.postId,
+ $inc:
+ commentsCount: 1
+
+ comment._id = Comments.insert comment
+
+ createCommentNotification comment
+
+ comment._id
diff --git a/lib/collections/notifications.coffee b/lib/collections/notifications.coffee
new file mode 100644
index 0000000..44e9344
--- /dev/null
+++ b/lib/collections/notifications.coffee
@@ -0,0 +1,16 @@
+@Notifications = new Mongo.Collection 'notifications'
+
+Notifications.allow
+ update: (userId, doc, fieldNames) ->
+ ownsDocument(userId, doc) and fieldNames.length is 1 and fieldNames[0] is 'read'
+
+@createCommentNotification = (comment) ->
+ post = Posts.findOne comment.postId
+ if comment.userId isnt post.userId
+ Notifications.insert
+ userId: post.userId
+ postId: post._id
+ commentId: comment._id
+ commenterName: comment.author
+ read: false
+
diff --git a/lib/collections/posts.coffee b/lib/collections/posts.coffee
new file mode 100644
index 0000000..5cb751c
--- /dev/null
+++ b/lib/collections/posts.coffee
@@ -0,0 +1,78 @@
+@Posts = new Mongo.Collection 'posts'
+
+Posts.allow
+ update: (userId, post) ->
+ ownsDocument userId, post
+ remove: (userId, post) ->
+ ownsDocument userId, post
+
+Posts.deny
+ update: (userId, post, fieldNames) ->
+ without = _.without fieldNames, 'url', 'title'
+ without.length > 0
+
+Posts.deny
+ update: (userId, post, fieldNames, modifier) ->
+ errors = validatePost modifier.$set
+ errors.title or errors.url
+
+@validatePost = (post) ->
+ errors = {}
+
+ if not post.url
+ errors.url = "Please fill in an URL"
+
+ if not post.title
+ errors.title = "Please fill a headline"
+
+ return errors
+
+Meteor.methods
+ postInsert: (postAttributes) ->
+ check this.userId, String
+ check postAttributes,
+ title: String
+ url: String
+
+ errors = validatePost postAttributes
+ if errors.title or errors.url
+ throw new Meteor.Error "invalid-post", "You must set a title and URL for your post"
+
+ postWithSameLink = Posts.findOne
+ url: postAttributes.url
+
+ if postWithSameLink
+ postExists: true
+ _id: postWithSameLink._id
+
+ user = Meteor.user()
+ post = _.extend postAttributes,
+ userId: user._id
+ author: user.username
+ submitted: new Date()
+ commentsCount: 0
+ upvoters: []
+ votes: 0
+ postId = Posts.insert post
+
+ _id: postId
+
+ upvote: (postId) ->
+ check this.userId, String
+ check postId, String
+
+ selector =
+ _id: postId
+ upvoters:
+ $ne: this.userId
+
+ filler =
+ $addToSet:
+ upvoters: this.userId
+ $inc:
+ votes: 1
+
+ affected = Posts.update selector, filler
+
+ if not affected
+ throw new Meteor.Error "invalid", "You weren't able to upvote that post"
diff --git a/lib/permissions.js.coffee b/lib/permissions.coffee
similarity index 52%
rename from lib/permissions.js.coffee
rename to lib/permissions.coffee
index 4b285c0..1c817ae 100644
--- a/lib/permissions.js.coffee
+++ b/lib/permissions.coffee
@@ -1,2 +1,2 @@
@ownsDocument = (userId, doc) ->
- doc && doc.userId == userId
+ doc && doc.userId is userId
diff --git a/lib/router.coffee b/lib/router.coffee
new file mode 100644
index 0000000..6bc8fad
--- /dev/null
+++ b/lib/router.coffee
@@ -0,0 +1,94 @@
+Router.configure
+ layoutTemplate: 'layout'
+ loadingTemplate: 'loading'
+ notFoundTemplate: 'notFound'
+ waitOn: ->
+ Meteor.subscribe 'notifications'
+
+PostsListController = RouteController.extend
+ template: 'postsList'
+ increment: 5
+ postsLimit: ->
+ parseInt(this.params.postsLimit) or this.increment
+ findOptions: ->
+ sort: this.sort
+ limit: this.postsLimit()
+ subscriptions: ->
+ this.postsSub = Meteor.subscribe 'posts', this.findOptions()
+ return
+ posts: ->
+ Posts.find {}, this.findOptions()
+ data: ->
+ self = this
+ output =
+ posts: self.posts()
+ ready: self.postsSub.ready
+ nextPath: ->
+ if self.posts().count() is self.postsLimit()
+ self.nextPath()
+ output
+
+
+@NewPostsController = PostsListController.extend
+ sort:
+ submitted: -1
+ _id: -1
+ nextPath: ->
+ Router.routes.newPosts.path
+ postsLimit: this.postsLimit() + this.increment
+
+
+@BestPostsController = PostsListController.extend
+ sort:
+ votes: -1
+ submitted: -1
+ _id: -1
+ nextPath: ->
+ Router.routes.bestPosts.path
+ postsLimit: this.postsLimit() + this.increment
+
+
+Router.route '/',
+ name: 'home'
+ controller: NewPostsController
+
+Router.route '/new/:postsLimit?',
+ name: 'newPosts'
+
+Router.route '/best/:postsLimit?',
+ name: 'bestPosts'
+
+Router.route '/posts/:_id',
+ name: 'postPage'
+ waitOn: ->
+ Meteor.subscribe 'singlePost', this.params._id
+ Meteor.subscribe 'comments', this.params._id
+ data: ->
+ Posts.findOne this.params._id
+
+Router.route '/posts/:_id/edit',
+ name: 'postEdit'
+ waitOn: ->
+ Meteor.subscribe 'singlePost', this.params._id
+ data: ->
+ Posts.findOne this.params._id
+
+Router.route '/submit',
+ name: 'postSubmit'
+
+@requireLogin = ->
+ if not Meteor.user()
+ if Meteor.loggingIn()
+ this.render this.loadingTemplate
+ else
+ this.render 'accessDenied'
+ else
+ this.next()
+
+Router.onBeforeAction 'dataNotFound',
+ only: 'postPage'
+
+Router.onBeforeAction requireLogin,
+ only: 'postSubmit'
+###
+###
diff --git a/package.json b/package.json
new file mode 100755
index 0000000..1e66dcd
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "microscope",
+ "private": true,
+ "scripts": {
+ "start": "meteor run"
+ },
+ "dependencies": {
+ "bcrypt": "^0.8.7",
+ "meteor-node-stubs": "~0.2.0"
+ }
+}
diff --git a/packages/.gitignore b/packages/.gitignore
deleted file mode 100644
index 513eb5c..0000000
--- a/packages/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-/router
-/page-js-ie-support
-/HTML5-History-API
-/accounts-ui-bootstrap-dropdown
-/paginated-subscription
-/spin
diff --git a/packages/errors b/packages/errors
deleted file mode 160000
index 3aa77ce..0000000
--- a/packages/errors
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 3aa77cea3740ea1ca32d7e5dd234eaa904e1393b
diff --git a/packages/smart.json b/packages/smart.json
deleted file mode 100644
index 5d5e1bf..0000000
--- a/packages/smart.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "packages": {}
-}
diff --git a/packages/smart.lock b/packages/smart.lock
deleted file mode 100644
index 7d0d807..0000000
--- a/packages/smart.lock
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "meteor": {},
- "dependencies": {
- "basePackages": {},
- "packages": {}
- }
-}
diff --git a/server/fixtures.js b/server/fixtures.js
new file mode 100644
index 0000000..64312c1
--- /dev/null
+++ b/server/fixtures.js
@@ -0,0 +1,72 @@
+// Fixture data
+if (Posts.find().count() === 0) {
+ var now = new Date().getTime();
+
+ // create two users
+ var tomId = Meteor.users.insert({
+ profile: { name: 'Tom Coleman' }
+ });
+ var tom = Meteor.users.findOne(tomId);
+ var sachaId = Meteor.users.insert({
+ profile: { name: 'Sacha Greif' }
+ });
+ var sacha = Meteor.users.findOne(sachaId);
+
+ var telescopeId = Posts.insert({
+ title: 'Introducing Telescope',
+ userId: sacha._id,
+ author: sacha.profile.name,
+ url: 'http://sachagreif.com/introducing-telescope/',
+ submitted: new Date(now - 7 * 3600 * 1000),
+ commentsCount: 2,
+ upvoters: [], votes: 0
+ });
+
+ Comments.insert({
+ postId: telescopeId,
+ userId: tom._id,
+ author: tom.profile.name,
+ submitted: new Date(now - 5 * 3600 * 1000),
+ body: 'Interesting project Sacha, can I get involved?'
+ });
+
+ Comments.insert({
+ postId: telescopeId,
+ userId: sacha._id,
+ author: sacha.profile.name,
+ submitted: new Date(now - 3 * 3600 * 1000),
+ body: 'You sure can Tom!'
+ });
+
+ Posts.insert({
+ title: 'Meteor',
+ userId: tom._id,
+ author: tom.profile.name,
+ url: 'http://meteor.com',
+ submitted: new Date(now - 10 * 3600 * 1000),
+ commentsCount: 0,
+ upvoters: [], votes: 0
+ });
+
+ Posts.insert({
+ title: 'The Meteor Book',
+ userId: tom._id,
+ author: tom.profile.name,
+ url: 'http://themeteorbook.com',
+ submitted: new Date(now - 12 * 3600 * 1000),
+ commentsCount: 0,
+ upvoters: [], votes: 0
+ });
+
+ for (var i = 0; i < 10; i++) {
+ Posts.insert({
+ title: 'Test post #' + i,
+ author: sacha.profile.name,
+ userId: sacha._id,
+ url: 'http://google.com/?q=test-' + i,
+ submitted: new Date(now - i * 3600 * 1000 + 1),
+ commentsCount: 0,
+ upvoters: [], votes: 0
+ });
+ }
+}
\ No newline at end of file
diff --git a/server/fixtures.js.coffee b/server/fixtures.js.coffee
deleted file mode 100644
index cb39cbf..0000000
--- a/server/fixtures.js.coffee
+++ /dev/null
@@ -1,83 +0,0 @@
-if Posts.find().count() == 0
- now = new Date().getTime()
-
- tomsId = Meteor.users.insert
- profile:
- name: 'Tom Coleman'
- tom = Meteor.users.findOne tomsId
-
- sachasId = Meteor.users.insert
- profile:
- name: 'Sacha Greif'
- sacha = Meteor.users.findOne sachasId
-
- nicholasId = Meteor.users.insert
- profile:
- name: 'Nicholas Hughes'
- nicholas = Meteor.users.findOne nicholasId
-
- telescopeId = Posts.insert
- title: 'Introducing Telescope'
- userId: sacha._id
- author: sacha.profile.name
- url: 'http://sachagreif.com/introducing-telescope'
- submitted: now - 7 * 3600 * 1000
- commentsCount: 2
- upvoters: []
- votes: 0
-
- Comments.insert
- postId: telescopeId
- userId: tom._id
- author: tom.profile.name
- submitted: now - 5 * 3600 * 1000
- body: 'Interesting project Sacha, can I get involved?'
-
- Comments.insert
- postId: telescopeId
- userId: sacha._id
- author: sacha.profile.name
- submitted: now - 3 * 3600 * 1000
- body: 'You sure can, Tom!'
-
- Posts.insert
- title: 'Meteor'
- userId: tom._id
- author: tom.profile.name
- url: 'http://meteor.com'
- submitted: now - 10 * 3600 * 1000
- commentsCount: 0
- upvoters: []
- votes: 0
-
- Posts.insert
- title: 'The Meteor Book'
- userId: tom._id
- author: tom.profile.name
- url: 'http://themeteorbook.com'
- submitted: now - 12 * 3600 * 1000
- commentsCount: 0
- upvoters: []
- votes: 0
-
- Posts.insert
- title: 'Making Games is Hard. So What?'
- userId: nicholas._id
- author: nicholas.profile.name
- url: 'http://makinggamesishard.piinecone.com'
- submitted: now - 3600 * 1000
- commentsCount: 0
- upvoters: []
- votes: 0
-
- createPost = (i) ->
- Posts.insert
- title: "Test post #{i}"
- userId: nicholas._id
- author: nicholas.profile.name
- url: "http://google.com?q=test-#{i}"
- submitted: now - i * 3600 * 1000
- commentsCount: 0
- upvoters: []
- votes: 0
- createPost(i) for i in [0..10]
diff --git a/server/main.js b/server/main.js
new file mode 100644
index 0000000..842d8bd
--- /dev/null
+++ b/server/main.js
@@ -0,0 +1,8 @@
+import { Meteor } from 'meteor/meteor';
+
+Meteor.startup(() => {
+ // code to run on server at startup
+});
+/*
+*/
+
diff --git a/server/publications.coffee b/server/publications.coffee
new file mode 100644
index 0000000..8b9678d
--- /dev/null
+++ b/server/publications.coffee
@@ -0,0 +1,19 @@
+Meteor.publish 'posts', (options) ->
+ check options,
+ sort: Object
+ limit: Number
+ return Posts.find {}, options
+
+Meteor.publish 'singlePost', (id) ->
+ check id, String
+ return Posts.find id
+
+Meteor.publish 'comments', (postId) ->
+ check postId, String
+ return Comments.find
+ postId: postId
+
+Meteor.publish 'notifications', ->
+ Notifications.find
+ userId: this.userId
+ read: false
diff --git a/server/publications.js.coffee b/server/publications.js.coffee
deleted file mode 100644
index 5ccb78f..0000000
--- a/server/publications.js.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-Meteor.publish 'newPosts', (limit) ->
- Posts.find({}, {sort: {submitted: -1}, limit: limit})
-
-Meteor.publish 'topPosts', (limit) ->
- Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit})
-
-Meteor.publish 'singlePost', (id) ->
- id && Posts.find id
-
-Meteor.publish 'comments', (postId) ->
- Comments.find postId: postId
-
-Meteor.publish 'notifications', ->
- Notifications.find userId: this.userId, read: false
diff --git a/smart.json b/smart.json
deleted file mode 100644
index 0954b12..0000000
--- a/smart.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "packages": {
- "router": {},
- "accounts-ui-bootstrap-dropdown": {},
- "paginated-subscription": {},
- "spin": {}
- }
-}
diff --git a/smart.lock b/smart.lock
deleted file mode 100644
index e5853a4..0000000
--- a/smart.lock
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "meteor": {},
- "dependencies": {
- "basePackages": {
- "router": {},
- "accounts-ui-bootstrap-dropdown": {},
- "paginated-subscription": {},
- "spin": {}
- },
- "packages": {
- "router": {
- "git": "https://github.com/tmeasday/meteor-router.git",
- "tag": "v0.5.4.1",
- "commit": "d00e380dda2184b70e909fc4a0fb5358d602c586"
- },
- "accounts-ui-bootstrap-dropdown": {
- "git": "https://github.com/erobit/meteor-accounts-ui-bootstrap-dropdown.git",
- "tag": "v0.6.5.1",
- "commit": "c8b29d2e7f8611d6dec9d6d23c1c2b94e000b0fb"
- },
- "paginated-subscription": {
- "git": "https://github.com/tmeasday/meteor-paginated-subscription.git",
- "tag": "v0.1.2",
- "commit": "35e0c6112df2b4cfeb60b559276a93cac5ee2dd6"
- },
- "spin": {
- "git": "https://github.com/SachaG/meteor-spin.git",
- "tag": "v0.2.2",
- "commit": "3f655f0016228e4195acc784c45d3e3a247dc39f"
- },
- "page-js-ie-support": {
- "git": "https://github.com/tmeasday/meteor-page-js-ie-support.git",
- "tag": "v1.3.5",
- "commit": "b99ed8380aefd10b2afc8f18d9eed4dd0d8ea9cb"
- },
- "HTML5-History-API": {
- "git": "https://github.com/tmeasday/meteor-HTML5-History-API.git",
- "tag": "v4.0.0",
- "commit": "dc4965f1424cfca625ec3fbea17eace03f8e32c5"
- }
- }
- }
-}