├── .meteor ├── .gitignore ├── release ├── platforms ├── .id ├── .finished-upgraders ├── packages └── versions ├── .gitignore ├── packages └── rss │ ├── .npm │ └── package │ │ ├── .gitignore │ │ ├── npm-shrinkwrap.json │ │ └── README │ ├── rss.js │ └── package.js ├── client ├── css │ ├── corpus │ │ ├── _grid.scss │ │ ├── _borders.scss │ │ ├── _buttons.scss │ │ ├── _colors.scss │ │ ├── _custom.scss │ │ ├── _depth.scss │ │ ├── _forms.scss │ │ ├── _images.scss │ │ ├── _reset.scss │ │ ├── _sidebar.scss │ │ ├── _sizing.scss │ │ ├── _syntax.scss │ │ ├── _animation.scss │ │ ├── _utilities.scss │ │ ├── _variables.scss │ │ ├── _breakpoints.scss │ │ ├── _positioning.scss │ │ ├── _typography.scss │ │ └── _whitespace.scss │ ├── _terms.scss │ ├── _dev.scss │ ├── _footer.scss │ ├── _variables.scss │ ├── _about.scss │ ├── _layout.scss │ ├── main.scss │ ├── _header.scss │ ├── _slack.scss │ ├── _loader.scss │ ├── _sweetalert.scss │ ├── _forms.scss │ ├── _login.scss │ └── _post-item.scss ├── posts │ ├── filepicker.js │ └── simply_countable.js ├── templates │ ├── post_page │ │ └── post_page.html │ ├── app │ │ ├── autoscroll.js │ │ ├── layout.html │ │ ├── intercom.js │ │ ├── not_found.html │ │ ├── loader.html │ │ ├── verify_email.js │ │ ├── segment.js │ │ └── terms.html │ ├── posts │ │ ├── posts_subscription.js │ │ └── posts_list.html │ ├── post_stars │ │ ├── posts_stars.js │ │ └── posts_stars.html │ ├── login │ │ ├── login.html │ │ ├── reset_password.html │ │ ├── login.js │ │ └── reset_password.js │ ├── footer │ │ ├── footer.js │ │ └── footer.html │ ├── slack │ │ └── slack.html │ ├── post_search │ │ ├── post_search.html │ │ └── posts_search.js │ ├── header │ │ ├── header.html │ │ └── header.js │ ├── post_item │ │ ├── post_item.html │ │ └── post_item.js │ ├── post_submit │ │ ├── post_submit.html │ │ └── post_submit.js │ ├── post_edit │ │ ├── post_edit.html │ │ └── post_edit.js │ └── about │ │ └── about.html └── main.html ├── lib ├── collections │ └── posts.js ├── counters.js ├── rss.js ├── api.js ├── validators.js ├── modal.js ├── router.js └── embedly.js ├── public └── img │ ├── favicon.ico │ ├── loading.gif │ ├── apple-touch-icon.png │ ├── edit-icon.svg │ ├── star-icon.svg │ ├── github.svg │ ├── waffle.svg │ ├── twitter.svg │ └── slack.svg ├── server ├── houston.js ├── publication.js ├── smtp.js ├── fixtures.js └── methods.js ├── scss.json └── README.md /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.0.2 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .meteor/local 2 | settings.json 3 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | browser 2 | ios 3 | server 4 | -------------------------------------------------------------------------------- /packages/rss/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/rss/rss.js: -------------------------------------------------------------------------------- 1 | RSS = Npm.require('rss'); 2 | -------------------------------------------------------------------------------- /client/css/corpus/_grid.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_grid.scss -------------------------------------------------------------------------------- /client/css/corpus/_borders.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_borders.scss -------------------------------------------------------------------------------- /client/css/corpus/_buttons.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_buttons.scss -------------------------------------------------------------------------------- /client/css/corpus/_colors.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_colors.scss -------------------------------------------------------------------------------- /client/css/corpus/_custom.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_custom.scss -------------------------------------------------------------------------------- /client/css/corpus/_depth.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_depth.scss -------------------------------------------------------------------------------- /client/css/corpus/_forms.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_forms.scss -------------------------------------------------------------------------------- /client/css/corpus/_images.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_images.scss -------------------------------------------------------------------------------- /client/css/corpus/_reset.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_reset.scss -------------------------------------------------------------------------------- /client/css/corpus/_sidebar.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_sidebar.scss -------------------------------------------------------------------------------- /client/css/corpus/_sizing.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_sizing.scss -------------------------------------------------------------------------------- /client/css/corpus/_syntax.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_syntax.scss -------------------------------------------------------------------------------- /lib/collections/posts.js: -------------------------------------------------------------------------------- 1 | Posts = new Mongo.Collection('posts'); 2 | -------------------------------------------------------------------------------- /client/css/corpus/_animation.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_animation.scss -------------------------------------------------------------------------------- /client/css/corpus/_utilities.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_utilities.scss -------------------------------------------------------------------------------- /client/css/corpus/_variables.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_variables.scss -------------------------------------------------------------------------------- /client/css/corpus/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_breakpoints.scss -------------------------------------------------------------------------------- /client/css/corpus/_positioning.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_positioning.scss -------------------------------------------------------------------------------- /client/css/corpus/_typography.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_typography.scss -------------------------------------------------------------------------------- /client/css/corpus/_whitespace.scss: -------------------------------------------------------------------------------- 1 | /Users/Jamie/dev/corpus/_whitespace.scss -------------------------------------------------------------------------------- /client/posts/filepicker.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | loadFilePicker(); 3 | }); 4 | -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiewilson/stylesheets/HEAD/public/img/favicon.ico -------------------------------------------------------------------------------- /public/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiewilson/stylesheets/HEAD/public/img/loading.gif -------------------------------------------------------------------------------- /server/houston.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | Houston.add_collection(Meteor.users); 3 | }); 4 | -------------------------------------------------------------------------------- /client/templates/post_page/post_page.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/templates/app/autoscroll.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | RouterAutoscroll.animationDuration = 0; 3 | }); 4 | -------------------------------------------------------------------------------- /public/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiewilson/stylesheets/HEAD/public/img/apple-touch-icon.png -------------------------------------------------------------------------------- /server/publication.js: -------------------------------------------------------------------------------- 1 | // publish all posts to the client 2 | Meteor.publish('posts', function() { 3 | return Posts.find(); 4 | }); 5 | -------------------------------------------------------------------------------- /client/css/_terms.scss: -------------------------------------------------------------------------------- 1 | .terms { 2 | h1, h2, h3, h4, h5 { 3 | font-weight: bold; 4 | } 5 | h1 { 6 | margin-bottom: $margin * 3; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scss.json: -------------------------------------------------------------------------------- 1 | { 2 | "outputStyle": "compressed", 3 | "enableAutoprefixer": true, 4 | "autoprefixerOptions": { 5 | "remove": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/templates/app/layout.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/css/_dev.scss: -------------------------------------------------------------------------------- 1 | #Mongol.MeteorToys { 2 | min-width: 200px !important; 3 | } 4 | 5 | #Mongol.Mongol_expand { 6 | width: 100% !important; 7 | max-width: 500px !important; 8 | } 9 | -------------------------------------------------------------------------------- /client/templates/posts/posts_subscription.js: -------------------------------------------------------------------------------- 1 | // meteorhacks:subs-manager 2 | var subs = new SubsManager(); 3 | // Get all the posts output by post publication 4 | subs.subscribe('posts'); 5 | -------------------------------------------------------------------------------- /client/templates/app/intercom.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | IntercomSettings.userInfo = function(user, info) { 3 | info.email = user.emails[0].address; 4 | info.verified = user.emails[0].verified; 5 | }; 6 | }); 7 | -------------------------------------------------------------------------------- /public/img/edit-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/templates/app/not_found.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /public/img/star-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/templates/app/loader.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/templates/post_stars/posts_stars.js: -------------------------------------------------------------------------------- 1 | Template.posts_stars.helpers({ 2 | // current user has upvoted this post 3 | upvoted: function() { 4 | var userId = Meteor.userId(); 5 | if (_.include(this.upvoters, userId)) 6 | return true; 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /client/css/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | align-self: flex-end; 3 | width: 100%; 4 | margin-top: 10%; 5 | @include small { 6 | flex-direction: column; 7 | } 8 | } 9 | 10 | .byline { 11 | @include small { 12 | margin-bottom: 1rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/templates/login/login.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /client/templates/footer/footer.js: -------------------------------------------------------------------------------- 1 | Template.footer.events({ 2 | 'click .js-logout': function() { 3 | Meteor.logout(function() { 4 | $("html, body").animate({ scrollTop: 0 }, "fast"); 5 | // track event with segment 6 | analytics.track('Logged Out'); 7 | }); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /packages/rss/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "rss", 3 | summary: "RSS feed generator", 4 | version: '0.1.0' 5 | }); 6 | 7 | Npm.depends({rss: '1.0.0'}); 8 | 9 | Package.onUse(function (api) { 10 | api.versionsFrom('0.9.4'); 11 | api.addFiles('rss.js', 'server'); 12 | api.export('RSS'); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/rss/.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "rss": { 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "mime": { 7 | "version": "1.3.4" 8 | }, 9 | "xml": { 10 | "version": "1.0.0" 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1gbq6w5ydb8dhydchlw 8 | -------------------------------------------------------------------------------- /client/templates/slack/slack.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /lib/counters.js: -------------------------------------------------------------------------------- 1 | truncateTitle = function() { 2 | $('[name=title]').simplyCountable({ 3 | counter: '.js-counter-title', 4 | maxCount: 70, 5 | strictMax: true 6 | }); 7 | } 8 | 9 | truncateDescription = function() { 10 | $('[name=description]').simplyCountable({ 11 | counter: '.js-counter-description', 12 | strictMax: true 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /client/templates/login/reset_password.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/css/_variables.scss: -------------------------------------------------------------------------------- 1 | // Overwrite default variables 2 | 3 | $base-font: 'proxima-nova', 'Avenir Next', Avenir, 'Helvetica Neue', Helvetica, sans-serif; 4 | $base-font-size: 0.875rem; 5 | $base-line-height: 1.2; 6 | 7 | /* see corpus/_variables.scss */ 8 | 9 | // Project Specific Variables 10 | 11 | $bounce: cubic-bezier(0.5, -0.5, 0.5, 1.5); 12 | $ease: cubic-bezier(0.65, 0.2, 0.35, 0.9); 13 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /packages/rss/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /client/css/_about.scss: -------------------------------------------------------------------------------- 1 | .h1-about { 2 | @include small { 3 | font-size: 1.6rem; 4 | } 5 | } 6 | 7 | .h2-about { 8 | @include small { 9 | font-size: 1.4rem; 10 | } 11 | } 12 | 13 | .social-cta { 14 | display: inline-block; 15 | margin-bottom: 1rem; 16 | margin-right: 1rem; 17 | width: 140px; 18 | @include small { 19 | width: 100%; 20 | } 21 | } 22 | 23 | .btn-squarecash { 24 | background-color: #22a301; 25 | } 26 | 27 | .btn-plasso { 28 | background-color: #232A42; 29 | } 30 | -------------------------------------------------------------------------------- /client/css/_layout.scss: -------------------------------------------------------------------------------- 1 | $container-width: 600px; 2 | $container-wider: 800px; 3 | $container-widest: 1000px; 4 | 5 | main { 6 | min-height: 100vh; 7 | margin-top: 5%; 8 | } 9 | 10 | .container { 11 | max-width: $container-width; 12 | width: $container-width; 13 | padding: 1rem; 14 | margin-right: auto; 15 | margin-left: auto; 16 | } 17 | 18 | .container.wider { 19 | max-width: $container-wider; 20 | width: $container-wider; 21 | } 22 | 23 | .container.widest { 24 | max-width: $container-widest; 25 | width: $container-widest; 26 | } 27 | -------------------------------------------------------------------------------- /client/templates/app/verify_email.js: -------------------------------------------------------------------------------- 1 | Template.layout.created = function() { 2 | if (Accounts._verifyEmailToken) { 3 | Accounts.verifyEmail(Accounts._verifyEmailToken, function(err) { 4 | if (err != null) { 5 | if (err.message = 'Verify email link expired [403]') { 6 | swal(err.message); 7 | } 8 | } else { 9 | swal("Thank you!", "Your email address has been confirmed", "success"); 10 | // track event with Segment 11 | analytics.track('Verified Email'); 12 | } 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /client/templates/footer/footer.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /client/templates/post_search/post_search.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /client/templates/posts/posts_list.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /client/templates/post_search/posts_search.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | Posts.initEasySearch(['title', 'link', 'description', 'createdBy'], { 3 | 'limit': 25, 4 | 'sort': function() { 5 | // most stars 6 | if (this.props.sortBy === 'stars') { 7 | return { 'stars': -1 }; 8 | // alphabetical 9 | } else if (this.props.sortBy === 'a-z') { 10 | return { 'title': 1 }; 11 | // reverse alphabetical 12 | } else if (this.props.sortBy === 'z-a') { 13 | return { 'title': -1 }; 14 | // oldest 15 | } else if (this.props.sortBy === 'oldest') { 16 | return { 'dateCreated': 1 }; 17 | } 18 | // default to Newest 19 | return { 'dateCreated': -1 }; 20 | }, 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/templates/post_stars/posts_stars.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /lib/rss.js: -------------------------------------------------------------------------------- 1 | Router.route('/feed.xml', { 2 | where: 'server', 3 | name: 'rss', 4 | action: function() { 5 | var feed = new RSS({ 6 | title: "Stylesheets", 7 | description: "The newest from the community-generated collection of CSS resources.", 8 | site_url: "https://stylesheets.co", 9 | feed_url: "https://stylesheets.co/feed.xml", 10 | image_url: "http://i.imgur.com/7jjEZXX.png" 11 | }); 12 | Posts.find({}, {sort: {submitted: -1}, limit: 20}).forEach(function(post) { 13 | feed.item({ 14 | title: post.title, 15 | description: post.description + " " + post.link, 16 | url: '/posts/' + post._id, 17 | date: post.dateCreated 18 | }) 19 | }); 20 | this.response.write(feed.xml()); 21 | this.response.end(); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /client/templates/login/login.js: -------------------------------------------------------------------------------- 1 | LoginComponents.loginCallback = function() { 2 | closeModal(); 3 | // track event with segment 4 | analytics.track('Logged In'); 5 | }; 6 | LoginComponents.signupCallback = function() { 7 | closeModal(); 8 | // track event with segment 9 | analytics.track('Signed Up'); 10 | }; 11 | 12 | LoginComponents.showTabs = false; 13 | 14 | Template.login.helpers({ 15 | resetPassword: function(){ 16 | return Session.get('resetPassword'); 17 | } 18 | }); 19 | 20 | Template.login.events({ 21 | 'blur #loginOrSignUp #email': function(e) { 22 | input = $.trim($(e.target).val()); 23 | $(e.target).val(input); 24 | } 25 | }); 26 | 27 | Template.loginOrSignUp.onRendered(function(){ 28 | $('#loginOrSignUp #email').attr('type', 'email'); 29 | $('#loginOrSignUp label').addClass('label'); 30 | }); 31 | -------------------------------------------------------------------------------- /client/css/main.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | @import "corpus/reset"; 4 | @import "corpus/breakpoints"; 5 | @import "corpus/animation"; 6 | @import "corpus/utilities"; 7 | @import "corpus/whitespace"; 8 | @import "corpus/grid"; 9 | @import "corpus/sidebar"; 10 | @import "corpus/sizing"; 11 | @import "corpus/colors"; 12 | @import "corpus/positioning"; 13 | @import "corpus/depth"; 14 | @import "corpus/typography"; 15 | @import "corpus/images"; 16 | @import "corpus/borders"; 17 | @import "corpus/forms"; 18 | @import "corpus/buttons"; 19 | @import "corpus/syntax"; 20 | 21 | @import "layout"; 22 | @import "login"; 23 | @import "header"; 24 | @import "footer"; 25 | @import "forms"; 26 | @import "about"; 27 | @import "slack"; 28 | @import "terms"; 29 | @import "post-item"; 30 | @import "loader"; 31 | @import "sweetalert"; 32 | 33 | @import "dev"; 34 | -------------------------------------------------------------------------------- /client/css/_header.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | min-height: 125px; 3 | padding: 1rem; 4 | @include large { 5 | padding: 3rem; 6 | flex-direction: row; 7 | } 8 | } 9 | 10 | .logo { 11 | text-transform: uppercase; 12 | letter-spacing: 5px; 13 | color: $black; 14 | font-weight: bold; 15 | padding: 1rem 0; 16 | transition: letter-spacing 200ms $bounce; 17 | @include large { 18 | padding: 0; 19 | &:hover { 20 | letter-spacing: 6px; 21 | } 22 | } 23 | 24 | } 25 | 26 | // needed to make sure logotype is centered 27 | // otherwise the tracking on the last letter 28 | // makes it look too far left of center 29 | .no-tracking { 30 | letter-spacing: 0; 31 | } 32 | 33 | .nav-link { 34 | display: inline-block; 35 | padding: 1rem; 36 | @include large { 37 | &:last-child { 38 | padding-right: 0; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/css/_slack.scss: -------------------------------------------------------------------------------- 1 | .slack-form { 2 | padding: 1rem; 3 | 4 | input { 5 | @extend .input; 6 | max-width: 300px !important; 7 | margin-bottom: 1rem !important; 8 | margin-right: 1rem; 9 | @include medium { 10 | margin-right: 0; 11 | } 12 | } 13 | 14 | button { 15 | width: 100%; 16 | max-width: 300px !important; 17 | } 18 | 19 | .slack-info { 20 | font-size: 1rem; 21 | color: $midgrey; 22 | margin-bottom: 2rem; 23 | } 24 | 25 | .slack-info br { 26 | display: none; 27 | } 28 | 29 | .message { 30 | background: $green; 31 | color: $white; 32 | padding: 1rem; 33 | font-size: 1rem; 34 | border-radius: $border-radius; 35 | animation: flip 200ms $bounce; 36 | max-width: 325px; 37 | margin: 1rem auto; 38 | } 39 | 40 | .message.error { 41 | background: $red; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/templates/login/reset_password.js: -------------------------------------------------------------------------------- 1 | if (Accounts._resetPasswordToken) { 2 | Session.set('resetPassword', Accounts._resetPasswordToken); 3 | } 4 | 5 | Template.reset_password.events({ 6 | 'submit #resetPasswordForm': function(e, t) { 7 | e.preventDefault(); 8 | 9 | var resetPasswordForm = $(e.currentTarget), 10 | password = resetPasswordForm.find('[name=password]').val(), 11 | passwordConfirm = resetPasswordForm.find('[name=password-confirm]').val(); 12 | 13 | if (isNotEmpty(password) && areValidPasswords(password, passwordConfirm)) { 14 | Accounts.resetPassword(Session.get('resetPassword'), password, function(err) { 15 | if (err) { 16 | swal("Oops. Something went wrong. Please try again.") 17 | } else { 18 | closeModal(); 19 | // track event with segment 20 | analytics.track('Reset Password'); 21 | } 22 | }); 23 | } 24 | return false; 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | Router.route('/api/posts', { 2 | where: 'server', 3 | name: 'apiPosts', 4 | action: function() { 5 | var parameters = this.request.query, 6 | // check if there is a limit parameter e.g. /api/posts?limit=5 7 | limit = !!parameters.limit ? parseInt(parameters.limit) : 50, 8 | data = Posts.find({}, { limit: limit, fields: {title: 1, link: 1, stars: 1, dateCreated: 1 }}).fetch(); 9 | this.response.write(JSON.stringify(data)); 10 | this.response.end(); 11 | } 12 | }); 13 | 14 | Router.route('/api/posts/:_id', { 15 | where: 'server', 16 | name: 'apiPost', 17 | action: function() { 18 | var post = Posts.findOne(this.params._id); 19 | if(post){ 20 | this.response.write(JSON.stringify(post)); 21 | } else { 22 | this.response.writeHead(404, {'Content-Type': 'text/html'}); 23 | this.response.write("Sorry, but that post cannot be found."); 24 | } 25 | this.response.end(); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client/templates/header/header.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /lib/validators.js: -------------------------------------------------------------------------------- 1 | trimInput = function(value) { 2 | return value.replace(/^\s*|\s*$/g, ''); 3 | }; 4 | 5 | isNotEmpty = function(value) { 6 | if (value && value !== ''){ 7 | return true; 8 | } 9 | swal('Please fill in all required fields.'); 10 | return false; 11 | }; 12 | 13 | isEmail = function(value) { 14 | var filter = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; 15 | if (filter.test(value)) { 16 | return true; 17 | } 18 | swal('Please enter a valid email address.'); 19 | return false; 20 | }; 21 | 22 | isValidPassword = function(password) { 23 | if (password.length < 6) { 24 | swal('Your password should be 6 characters or longer.'); 25 | return false; 26 | } 27 | return true; 28 | }; 29 | 30 | areValidPasswords = function(password, confirm) { 31 | if (!isValidPassword(password)) { 32 | return false; 33 | } 34 | if (password !== confirm) { 35 | swal('Your two passwords are not equivalent.'); 36 | return false; 37 | } 38 | return true; 39 | }; 40 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | # core 8 | iron:router 9 | http 10 | standard-minifiers 11 | meteor-base 12 | mobile-experience 13 | mongo 14 | blaze-html-templates 15 | session 16 | jquery 17 | tracker 18 | logging 19 | reload 20 | random 21 | ejson 22 | spacebars 23 | check 24 | accounts-password 25 | email 26 | force-ssl 27 | 28 | # third-party 29 | fourseven:scss@1.0.0 30 | fongandrew:login-components 31 | kevohagan:sweetalert 32 | iamkevingreen:imagesloaded 33 | matteodem:easy-search 34 | natestrauser:filepicker-plus 35 | okgrow:router-autoscroll 36 | rounce:fastclick 37 | meteorhacks:fast-render 38 | meteorhacks:subs-manager 39 | houston:admin 40 | percolatestudio:segment.io 41 | percolate:intercom 42 | jparker:crypto-md5 43 | studiointeract:slack-invite 44 | mrt:twit 45 | 46 | # local 47 | rss 48 | -------------------------------------------------------------------------------- /client/css/_loader.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | width: 50px; 3 | height: 50px; 4 | align-self: center; 5 | } 6 | 7 | .circular { 8 | animation: rotate 1s linear infinite; 9 | height: 50px; 10 | overflow: visible; 11 | width: 50px; 12 | } 13 | 14 | .path { 15 | stroke: $white; 16 | stroke-dasharray: 1,200; 17 | stroke-dashoffset: 0; 18 | stroke-linecap: round; 19 | animation: dash 1.5s ease-in-out infinite; 20 | } 21 | 22 | @keyframes rotate { 23 | 100% { transform: rotate(360deg); } 24 | } 25 | 26 | @keyframes rotate { 27 | 100% { transform: rotate(360deg); } 28 | } 29 | 30 | @keyframes dash { 31 | 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; } 32 | 50% { stroke-dasharray: 89, 200; stroke-dashoffset: -35; } 33 | 100% { stroke-dasharray: 89, 200; stroke-dashoffset: -124; } 34 | } 35 | @keyframes dash { 36 | 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; } 37 | 50% { stroke-dasharray: 89, 200; stroke-dashoffset: -35; } 38 | 100% { stroke-dasharray: 89, 200; stroke-dashoffset: -124; } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /lib/modal.js: -------------------------------------------------------------------------------- 1 | openModal = function() { 2 | $('.modal').addClass('is-open'); 3 | $('body').addClass('overflow-hidden'); 4 | setTimeout(function() { 5 | $('#loginOrSignUp input').first().focus(); 6 | }, 100); 7 | } 8 | 9 | closeModal = function() { 10 | $('.modal').removeClass('is-open'); 11 | $('body').removeClass('overflow-hidden'); 12 | $('#loginOrSignUp')[0].reset(); 13 | } 14 | 15 | verifyEmailModal = function() { 16 | swal({ 17 | title: "Please verify your account.", 18 | text: "You should have reveived a verification email when you created your account.", 19 | type: "warning", 20 | showCancelButton: true, 21 | cancelButtonText: "I'll take a look.", 22 | confirmButtonText: "Resend?", 23 | closeOnConfirm: false 24 | }, function() { 25 | var userId = Meteor.userId(); 26 | Meteor.call('sendVerificationEmail', userId, function (error, result) { 27 | if (error) { 28 | swal(error.reason); 29 | } else { 30 | swal("Check your email!", "We've resent your verification email", "success"); 31 | } 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /client/templates/app/segment.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | analytics.load(Meteor.settings.public.segmentKey); 3 | Tracker.autorun(function(c) { 4 | // waiting for user subscription to load 5 | if (! Router.current() || ! Router.current().ready()) 6 | return; 7 | 8 | var user = Meteor.user(); 9 | if (! user) 10 | return; 11 | 12 | analytics.identify(user._id, { 13 | email: user.emails[0].address 14 | }); 15 | 16 | c.stop(); 17 | }); 18 | }); 19 | 20 | Template.posts_list.onRendered(function() { 21 | analytics.page("Home"); 22 | }); 23 | 24 | Template.post_submit.onRendered(function() { 25 | analytics.page("Submit"); 26 | }); 27 | 28 | Template.post_edit.onRendered(function() { 29 | analytics.page("Edit"); 30 | }); 31 | 32 | Template.about.onRendered(function() { 33 | analytics.page("About"); 34 | }); 35 | 36 | Template.posts_stars.onRendered(function() { 37 | analytics.page("Stars"); 38 | }); 39 | 40 | Template.login.onRendered(function() { 41 | analytics.page("Login"); 42 | }); 43 | 44 | Template.slack.onRendered(function() { 45 | analytics.page("Slack"); 46 | }); 47 | 48 | Template.terms.onRendered(function() { 49 | analytics.page("Terms"); 50 | }); 51 | -------------------------------------------------------------------------------- /client/css/_sweetalert.scss: -------------------------------------------------------------------------------- 1 | .sweet-overlay { 2 | background: fade-out($black, 0.1); 3 | } 4 | 5 | .sweet-alert { 6 | font-family: $base-font; 7 | $text-color: $midgrey; 8 | padding: ($padding * 3) ($padding * 2); 9 | text-align: center; 10 | border-radius: $border-3; 11 | 12 | .sa-icon { margin: 0 auto 1rem auto; } 13 | 14 | h2 { 15 | font-size: 1.5rem; 16 | line-height: 1.2; 17 | margin-bottom: 0.5rem; 18 | } 19 | 20 | button { 21 | border: none !important; 22 | box-shadow: none !important; 23 | border-radius: $border-3; 24 | 25 | &.confirm { background-color: $accent !important; } 26 | &.cancel { background-color: $grey !important; } 27 | } 28 | 29 | input { 30 | border-color: $border-color; 31 | border-style: $border-style; 32 | border-width: $border-1; 33 | outline: none; 34 | padding: $padding; 35 | width: 100%; 36 | transition: border-color 200ms; 37 | 38 | &:focus { 39 | border-color: $midgrey; 40 | box-shadow: none; 41 | } 42 | } 43 | } 44 | 45 | @keyframes showSweetAlert { 46 | 0% { transform: scale(0.85); } 47 | 100% { transform: scale(1); } 48 | } 49 | 50 | @keyframes hideSweetAlert { 51 | 0% { transform: scale(1) } 52 | 100% { transform: scale(0.85); } 53 | } 54 | -------------------------------------------------------------------------------- /client/css/_forms.scss: -------------------------------------------------------------------------------- 1 | .btn-save { 2 | @include small { 3 | margin-right: 0; 4 | } 5 | } 6 | 7 | .label { 8 | color: $midgrey; 9 | width: 100%; 10 | position: relative; 11 | } 12 | 13 | .label-note { 14 | color: darken($grey, 15%); 15 | font-size: smaller; 16 | position: absolute; 17 | right: 0; 18 | bottom: 0; 19 | } 20 | 21 | //==================================================== 22 | // Image Preview 23 | //==================================================== 24 | 25 | .image-thumbnail { 26 | height: 3rem; 27 | width: 4rem; 28 | border-radius: $border-radius; 29 | background: $white url(../img/loading.gif) no-repeat center; 30 | margin-right: 1rem; 31 | object-fit: cover; 32 | object-position: left; 33 | } 34 | 35 | .image-thumbnail[src=""] { 36 | display: none; 37 | } 38 | 39 | //==================================================== 40 | // Search 41 | //==================================================== 42 | 43 | .input.search-input { 44 | border-width: 0; 45 | min-height: 60px; 46 | border-right: 2px solid $lightgrey; 47 | -webkit-appearance: none; 48 | &:focus { 49 | border-color: darken($lightgrey, 5%); 50 | } 51 | @include large { 52 | padding-left: 3rem; 53 | } 54 | } 55 | 56 | .select { 57 | border-width: 0; 58 | min-height: 60px; 59 | min-width: 125px; 60 | } 61 | -------------------------------------------------------------------------------- /client/templates/header/header.js: -------------------------------------------------------------------------------- 1 | Template.header.helpers({ 2 | starCount: function() { 3 | var user = Meteor.userId(); 4 | // get number of stars current user has 5 | return Posts.find({upvoters: { $in: [user] }}).count(); 6 | } 7 | }); 8 | 9 | Template.header.events({ 10 | 'click .js-submit': function(e) { 11 | e.preventDefault(); 12 | if (Meteor.user().emails[0].verified === true) { 13 | Router.go('submit') 14 | } else { 15 | verifyEmailModal(); 16 | } 17 | }, 18 | // clear input when click on logo 19 | 'click .js-clear-search': function() { 20 | // Get instance of search based on index 21 | var instance = EasySearch.getComponentInstance({ index: 'posts' }); 22 | // if someone has searched, clear instance and reset searchbar 23 | if ($('.search-input').val()) 24 | instance.clear(); 25 | $('.search-input').val(""); 26 | }, 27 | 'click .js-login': function(e) { 28 | openModal(); 29 | }, 30 | 'click #loginOrSignUp, click #resetPasswordForm': function(e) { 31 | e.stopPropagation(); 32 | }, 33 | 'click .is-open': function() { 34 | closeModal(); 35 | }, 36 | 'change .js-sort-select': function(e) { 37 | var instance = EasySearch.getComponentInstance({ index: 'posts' }); 38 | EasySearch.changeProperty('posts', 'sortBy', $(e.target).children(':selected').data('sort')); 39 | instance.triggerSearch(); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /client/templates/post_item/post_item.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /client/templates/post_submit/post_submit.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | // meteorhacks:subs-manager 2 | var subs = new SubsManager(); 3 | 4 | // Apply layout template to all routes 5 | // wait on posts before loading 6 | Router.configure({ 7 | layoutTemplate: 'layout', 8 | loadingTemplate: 'loader', 9 | notFoundTemplate: 'not_found' 10 | }); 11 | 12 | // Route index to posts list 13 | Router.route('/', { 14 | template: 'posts_list', 15 | name: 'home', 16 | waitOn: function() { 17 | return subs.subscribe('posts'); 18 | }, 19 | fastRender: true 20 | }); 21 | 22 | Router.route('/stars', { 23 | template: 'posts_stars', 24 | name: 'stars', 25 | fastRender: true 26 | }); 27 | 28 | Router.route('/submit', { 29 | template: 'post_submit', 30 | name: 'submit' 31 | }); 32 | 33 | Router.route('/posts/:_id', { 34 | template: 'post_page', 35 | name: 'post', 36 | data: function() { return Posts.findOne(this.params._id); } 37 | }); 38 | 39 | Router.route('/posts/:_id/edit', { 40 | template: 'post_edit', 41 | name: 'edit', 42 | data: function() { return Posts.findOne(this.params._id); } 43 | }); 44 | 45 | Router.route('/about', { 46 | template: 'about' 47 | }); 48 | 49 | Router.route('/slack', { 50 | template: 'slack' 51 | }); 52 | 53 | Router.route('/terms', { 54 | template: 'terms' 55 | }); 56 | 57 | Router.route('/loader', { 58 | template: 'loader' 59 | }); 60 | 61 | var requireLogin = function() { 62 | if (! Meteor.user()) { 63 | if (Meteor.loggingIn()) { 64 | this.render(this.loading); 65 | } else { 66 | Router.go('home'); 67 | } 68 | } else { 69 | this.next(); 70 | } 71 | }; 72 | 73 | if (Meteor.isClient){ 74 | Router.onBeforeAction(requireLogin, { only: ['submit', 'edit', 'stars'] }); 75 | } 76 | -------------------------------------------------------------------------------- /client/templates/post_edit/post_edit.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /server/smtp.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function () { 2 | smtp = { 3 | username: 'jamie@mg.stylesheets.co', 4 | password: Meteor.settings.mailgunPassword, 5 | server: 'smtp.mailgun.org', // eg: mail.gandi.net 6 | port: 25 7 | } 8 | 9 | process.env.MAIL_URL = 'smtp://' + encodeURIComponent(smtp.username) + ':' + encodeURIComponent(smtp.password) + '@' + encodeURIComponent(smtp.server) + ':' + smtp.port; 10 | 11 | // By default, the email is sent from no-reply@meteor.com. If you wish to receive email from users asking for help with their account, be sure to set this to an email address that you can receive email at. 12 | Accounts.emailTemplates.from = 'Stylesheets '; 13 | 14 | // The public name of your application. Defaults to the DNS name of the application (eg: awesome.meteor.com). 15 | Accounts.emailTemplates.siteName = 'Stylesheets'; 16 | 17 | // A Function that takes a user object and returns a String for the subject line of the email. 18 | Accounts.emailTemplates.verifyEmail.subject = function(user) { 19 | return 'Welcome to Stylesheets!'; 20 | }; 21 | 22 | // A Function that takes a user object and a url, and returns the body text for the email. 23 | // Note: if you need to return HTML instead, use Accounts.emailTemplates.verifyEmail.html 24 | Accounts.emailTemplates.verifyEmail.html = function(user, url) { 25 | return '

Please click on the following link to verify your email address:

Verify Your Stylesheets Account'; 26 | }; 27 | 28 | }); 29 | 30 | // SEND A VERIFICATION EMAIL 31 | Accounts.onCreateUser(function(options, user) { 32 | 33 | // we wait for Meteor to create the user before sending an email 34 | Meteor.setTimeout(function() { 35 | Accounts.sendVerificationEmail(user._id); 36 | }, 2 * 1000); 37 | 38 | return user; 39 | }); 40 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stylesheets 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /client/css/_login.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | opacity: 0; 3 | visibility: hidden; 4 | transform: scale(0.95); 5 | transition-property: opacity, visibility, transform; 6 | transition-timing-function: $ease; 7 | transition-duration: 200ms; 8 | 9 | display: flex !important; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | 14 | position: fixed; 15 | top: 0; 16 | right: 0; 17 | left: 0; 18 | right: 0; 19 | height: 100vh; 20 | width: 100%; 21 | 22 | background: fade-out($black, 0.1); 23 | padding: 1rem; 24 | overflow-y: scroll; 25 | } 26 | 27 | .modal.is-open { 28 | opacity: 1; 29 | visibility: visible; 30 | transform: scale(1); 31 | } 32 | 33 | #loginOrSignUp, 34 | #resetPasswordForm { 35 | border-radius: $border-radius; 36 | opacity: 0; 37 | transform: translateY(1.5rem); 38 | transition-duration: 500ms; 39 | transition-property: transform, opacity; 40 | @include small { 41 | width: 100%; 42 | } 43 | } 44 | 45 | .is-open #loginOrSignUp, 46 | .is-open #resetPasswordForm { 47 | opacity: 1; 48 | transform: translateY(0); 49 | } 50 | 51 | #loginOrSignUp, 52 | #resetPasswordForm { 53 | background: $silver; 54 | padding: 2rem; 55 | } 56 | 57 | #loginOrSignUp input, 58 | #resetPasswordForm input[type='password'] { 59 | border-color: $border-color; 60 | border-style: $border-style; 61 | border-width: $border-1; 62 | outline: none; 63 | padding: 0.75rem; 64 | margin-bottom: 1rem; 65 | width: 100%; 66 | transition: border-color 200ms; 67 | 68 | &:focus { 69 | border-color: $accent; 70 | } 71 | } 72 | 73 | .save-btn, 74 | .btn-submit { 75 | width: 100%; 76 | height: auto; 77 | } 78 | 79 | .mode-links { 80 | margin-top: 2rem; 81 | display: flex; 82 | justify-content: space-between; 83 | cursor: pointer; 84 | font-size: smaller; 85 | color: $midgrey; 86 | } 87 | 88 | .login-link, 89 | .sign-up-link { 90 | text-decoration: underline; 91 | } 92 | 93 | .success, 94 | .error { 95 | background: $red; 96 | color: $white; 97 | text-align: center; 98 | line-height: 1; 99 | padding: 0.5rem; 100 | border-radius: $border-radius; 101 | animation: shake 200ms $bounce; 102 | width: 100%; 103 | display: block; 104 | margin-bottom: 1rem; 105 | } 106 | 107 | .success { 108 | background: $green; 109 | color: $black; 110 | animation: flip 200ms $bounce; 111 | } 112 | -------------------------------------------------------------------------------- /client/templates/post_submit/post_submit.js: -------------------------------------------------------------------------------- 1 | Template.post_submit.events({ 2 | // lib/embedly.js 3 | 'blur [name=link]': function() { 4 | fillEmbedlyData(); 5 | }, 6 | // lib/counters.js 7 | 'keydown [name=title]': function() { 8 | truncateTitle(); 9 | }, 10 | // lib/counters.js 11 | 'keydown [name=description]': function() { 12 | truncateDescription(); 13 | }, 14 | 'click .filepicker': function (e) { 15 | filepicker.pick({ 16 | mimetypes: ['image/gif','image/jpeg','image/png'], 17 | maxSize: 2*1024*1024, 18 | cropRatio: 4/3, 19 | imageDim: [400, 300], 20 | cropForce: true, 21 | multiple: false 22 | }, 23 | function(blob){ 24 | $('[name=image]').val(blob.url); 25 | $('.image-thumbnail').attr('src', blob.url); 26 | $('.filepicker').text('Replace Image'); 27 | }); 28 | }, 29 | 30 | 'submit form': function (e) { 31 | e.preventDefault(); 32 | 33 | // place input values in postProperties 34 | var postProperties = { 35 | link: $(e.target).find('[name=link]').val(), 36 | title: $(e.target).find('[name=title]').val(), 37 | description: $(e.target).find('[name=description]').val(), 38 | color: $(e.target).find('[name=color]').val(), 39 | image: $(e.target).find('[name=image]').val() 40 | }; 41 | 42 | // check if user email is verified 43 | if (Meteor.user().emails[0].verified === true) { 44 | Meteor.call('submitPost', postProperties, function (error, result) { 45 | // abort and alert error reason 46 | if (error) { 47 | sweetAlert(error.reason); 48 | } 49 | // tell user the link in already posted 50 | if (result.postExists) { 51 | sweetAlert({ 52 | title: "D’oh! That’s already been posted.", 53 | type: "info", 54 | confirmButtonText: "Take a look. And upvote it!" 55 | }, function() { 56 | Router.go('post', {_id: result._id}); 57 | }); 58 | } else { 59 | // if successfully posted, go to new post page 60 | sweetAlert({ 61 | title: "Thanks for posting!", 62 | type: "success", 63 | confirmButtonText: "View the latest posts." 64 | }, function() { 65 | Router.go('home'); 66 | }); 67 | // post a tweet by @stylesheetsco 68 | Meteor.call('postTweet', postProperties); 69 | // track event with segment 70 | analytics.track('Submitted Post'); 71 | } 72 | }); 73 | // if not verified allow resending 74 | } else { 75 | verifyEmailModal(); 76 | } 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /client/templates/post_item/post_item.js: -------------------------------------------------------------------------------- 1 | Template.post_item.helpers({ 2 | // current user created this post 3 | ownsPost: function() { 4 | return this.createdBy === Meteor.userId(); 5 | }, 6 | // current user has upvoted this post 7 | upvoted: function() { 8 | var userId = Meteor.userId(); 9 | if (_.include(this.upvoters, userId)) 10 | return true; 11 | }, 12 | hash: function() { 13 | url = this.link; 14 | secret = Meteor.settings.public.screenshotMachineSecret; 15 | var hash = CryptoJS.MD5(url + secret).toString(); 16 | return hash; 17 | } 18 | }); 19 | 20 | Template.post_item.onRendered(function(){ 21 | // initialize imagesLoaded on screenshot elements 22 | var screenshot = imagesLoaded(".screenshot"); 23 | // remove loading animation as each image loads 24 | screenshot.on('progress', function(instance, image) { 25 | // TOOD: get loading item without prev() 26 | var item = $(image.img).prev(); 27 | item.fadeOut(300); 28 | }); 29 | }); 30 | 31 | Template.post_item.events({ 32 | 'click .js-upvote': function(e) { 33 | e.preventDefault(); 34 | e.stopPropagation(); 35 | var userId = Meteor.userId(); 36 | // if user is logged in 37 | if (userId && (Meteor.user().emails[0].verified === true)) { 38 | // call upvote meteor method on click 39 | Meteor.call('upvote', this._id); 40 | // track event with Segment 41 | analytics.track('Upvoted'); 42 | } else if (userId && Meteor.user().emails[0].verified === false) { 43 | // give user option to resend verify email 44 | verifyEmailModal(); 45 | } else { 46 | // open login modal instead 47 | openModal(); 48 | } 49 | }, 50 | 'click .js-downvote': function(e) { 51 | e.preventDefault(); 52 | e.stopPropagation(); 53 | // if user is logged in 54 | var userId = Meteor.userId(); 55 | if (userId && (Meteor.user().emails[0].verified === true)) { 56 | // call upvote meteor method on click 57 | Meteor.call('downvote', this._id); 58 | // track event with Segment 59 | analytics.track('Downvoted'); 60 | } else if (userId && Meteor.user().emails[0].verified === false) { 61 | // give user option to resend verify email 62 | verifyEmailModal(); 63 | } else { 64 | // open login modal instead 65 | openModal(); 66 | } 67 | }, 68 | 'click .js-edit': function(e) { 69 | e.preventDefault(); 70 | e.stopPropagation(); 71 | var currentPostId = this._id; 72 | var userId = Meteor.userId(); 73 | if (userId && (Meteor.user().emails[0].verified === true)) { 74 | Router.go('edit', {_id: currentPostId}); 75 | } else { 76 | verifyEmailModal(); 77 | } 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.1 2 | accounts-password@1.1.3 3 | autoupdate@1.2.3 4 | babel-compiler@5.8.24_1 5 | babel-runtime@0.1.4 6 | base64@1.0.4 7 | binary-heap@1.0.4 8 | blaze@2.1.3 9 | blaze-html-templates@1.0.1 10 | blaze-tools@1.0.4 11 | boilerplate-generator@1.0.4 12 | caching-compiler@1.0.0 13 | caching-html-compiler@1.0.2 14 | callback-hook@1.0.4 15 | check@1.0.6 16 | chuangbo:cookie@1.1.0 17 | coffeescript@1.0.10 18 | dburles:mongo-collection-instances@0.3.4 19 | ddp@1.2.2 20 | ddp-client@1.2.1 21 | ddp-common@1.2.1 22 | ddp-rate-limiter@1.0.0 23 | ddp-server@1.2.1 24 | deps@1.0.9 25 | diff-sequence@1.0.1 26 | ecmascript@0.1.5 27 | ecmascript-collections@0.1.6 28 | ejson@1.0.7 29 | email@1.0.7 30 | fastclick@1.0.7 31 | fongandrew:instance-vars@0.2.0 32 | fongandrew:login-components@0.2.0 33 | fongandrew:save-button@0.1.0 34 | fongandrew:spacebars-helpers@0.1.0 35 | force-ssl@1.0.6 36 | fourseven:scss@1.2.3 37 | geojson-utils@1.0.4 38 | handlebars@1.0.4 39 | hot-code-push@1.0.0 40 | houston:admin@2.0.5 41 | html-tools@1.0.5 42 | htmljs@1.0.5 43 | http@1.1.1 44 | iamkevingreen:imagesloaded@1.0.4 45 | id-map@1.0.4 46 | iron:controller@1.0.8 47 | iron:core@1.0.8 48 | iron:dynamic-template@1.0.8 49 | iron:layout@1.0.8 50 | iron:location@1.0.9 51 | iron:middleware-stack@1.0.9 52 | iron:router@1.0.9 53 | iron:url@1.0.9 54 | jparker:crypto-core@0.1.0 55 | jparker:crypto-md5@0.1.1 56 | jquery@1.11.4 57 | kevohagan:sweetalert@1.0.0 58 | lai:collection-extensions@0.1.4 59 | launch-screen@1.0.4 60 | livedata@1.0.15 61 | localstorage@1.0.5 62 | logging@1.0.8 63 | matteodem:easy-search@1.6.4 64 | meteor@1.1.9 65 | meteor-base@1.0.1 66 | meteor-platform@1.2.3 67 | meteorhacks:aggregate@1.3.0 68 | meteorhacks:collection-utils@1.2.0 69 | meteorhacks:fast-render@2.10.0 70 | meteorhacks:inject-data@1.4.1 71 | meteorhacks:picker@1.0.3 72 | meteorhacks:subs-manager@1.6.2 73 | minifiers@1.1.7 74 | minimongo@1.0.10 75 | mobile-experience@1.0.1 76 | mobile-status-bar@1.0.6 77 | mongo@1.1.2 78 | mongo-id@1.0.1 79 | mongo-livedata@1.0.9 80 | mrt:twit@0.2.0 81 | natestrauser:filepicker-plus@2.0.0 82 | npm-bcrypt@0.7.8_2 83 | npm-mongo@1.4.39_1 84 | observe-sequence@1.0.7 85 | okgrow:router-autoscroll@0.1.0 86 | ordered-dict@1.0.4 87 | percolate:intercom@1.4.2 88 | percolatestudio:segment.io@3.0.0 89 | promise@0.5.0 90 | random@1.0.4 91 | rate-limit@1.0.0 92 | reactive-dict@1.1.2 93 | reactive-var@1.0.6 94 | reload@1.1.4 95 | retry@1.0.4 96 | rounce:fastclick@1.0.3 97 | routepolicy@1.0.6 98 | rss@0.1.0 99 | service-configuration@1.0.5 100 | session@1.1.1 101 | sha@1.0.4 102 | spacebars@1.0.7 103 | spacebars-compiler@1.0.7 104 | srp@1.0.4 105 | standard-minifiers@1.0.1 106 | studiointeract:slack-invite@1.0.5 107 | templating@1.1.4 108 | templating-tools@1.0.0 109 | tmeasday:paginated-subscription@0.2.4 110 | tracker@1.0.9 111 | ui@1.0.8 112 | underscore@1.0.4 113 | url@1.0.5 114 | webapp@1.2.2 115 | webapp-hashing@1.0.5 116 | -------------------------------------------------------------------------------- /client/css/_post-item.scss: -------------------------------------------------------------------------------- 1 | $screenshot-height: 188px; 2 | 3 | .post-item { 4 | position: relative; 5 | background: $white; 6 | margin-bottom: 1rem; 7 | align-self: flex-start; 8 | &:focus { 9 | outline: none; 10 | } 11 | @include large { 12 | animation: fadeInUp 300ms; 13 | transform: translate3d(0, 0, 0); 14 | transform-origin: bottom; 15 | -webkit-backface-visibility: hidden; 16 | box-shadow: 0 6px 12px -4px rgba(36, 45, 50, 0.1); 17 | margin: 1rem; 18 | min-width: 250px; 19 | max-width: 250px 20 | } 21 | } 22 | 23 | .post-item:nth-child(3n+3), 24 | .post-item:nth-child(5n+1) { 25 | animation-duration: 500ms; } 26 | 27 | .star-badge { 28 | font-size: small; 29 | background: fade-out($black, 0.25); 30 | color: $white; 31 | padding: 0.4rem 0.75rem 0.5rem; 32 | transition: padding 100ms $bounce; 33 | &:hover { 34 | padding: 0.6rem 1rem 0.8rem; 35 | } 36 | @include small { 37 | padding: 0.6rem 1rem 0.8rem; 38 | } 39 | } 40 | 41 | .star-icon { 42 | vertical-align: top; 43 | margin-right: 0.25rem; 44 | path { 45 | fill: currentColor; 46 | } 47 | } 48 | 49 | .star-count { 50 | line-height: 1; 51 | vertical-align: bottom; 52 | } 53 | 54 | .js-upvote .star-icon { 55 | animation: scaleUp 200ms $bounce; 56 | } 57 | 58 | .js-downvote .star-icon { 59 | color: $accent; 60 | animation: fadeInUp 200ms $bounce; 61 | } 62 | 63 | .screenshot-wrap { 64 | line-height: 0; 65 | } 66 | 67 | .screenshot-loader { 68 | background: $silver; 69 | opacity: 0.8; 70 | } 71 | 72 | .screenshot-loader .loader { 73 | position: absolute; 74 | z-index: 1000; 75 | top: 0; 76 | right: 0; 77 | bottom: 0; 78 | left: 0; 79 | margin: auto; 80 | } 81 | 82 | .screenshot-loader .path { 83 | stroke: fade-out($white, 0.5); 84 | } 85 | 86 | .screenshot { 87 | min-height: $screenshot-height; 88 | filter: brightness(0.85) contrast(0.85); 89 | object-fit: cover; 90 | } 91 | 92 | .post-title { 93 | font-size: 1rem; 94 | margin-bottom: 0.5rem; 95 | @include small { 96 | font-weight: 600; 97 | } 98 | } 99 | 100 | .post-description { 101 | font-size: 0.875rem; 102 | color: $midgrey; 103 | @include large { 104 | color: darken($grey, 15%); 105 | } 106 | } 107 | 108 | .post-edit { 109 | cursor: pointer; 110 | height: 42px; 111 | width: 42px; 112 | position: absolute; 113 | bottom: 0; 114 | right: 0; 115 | @include small { 116 | height: 60px; 117 | width: 60px; 118 | } 119 | } 120 | 121 | .edit-wrap { 122 | height: 100%; 123 | width: 100%; 124 | } 125 | 126 | .edit-background, 127 | .edit-icon { 128 | transition: fill 300ms; 129 | } 130 | 131 | .edit-background { 132 | fill: $silver; 133 | } 134 | 135 | .edit-icon { 136 | fill: $grey; 137 | } 138 | 139 | .post-edit:hover { 140 | .edit-background { 141 | fill: $accent; 142 | } 143 | .edit-icon { 144 | fill: $white; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /client/templates/post_edit/post_edit.js: -------------------------------------------------------------------------------- 1 | Template.post_edit.helpers({ 2 | hash: function() { 3 | url = this.link; 4 | secret = Meteor.settings.public.screenshotMachineSecret; 5 | var hash = CryptoJS.MD5(url + secret).toString(); 6 | return hash; 7 | } 8 | }); 9 | 10 | Template.post_edit.events({ 11 | // lib/counters.js 12 | 'keydown [name=title]': function() { 13 | truncateTitle(); 14 | }, 15 | // lib/counters.js 16 | 'keydown [name=description]': function() { 17 | truncateDescription(); 18 | }, 19 | 'click .filepicker': function (e) { 20 | filepicker.pick({ 21 | mimetypes: ['image/gif','image/jpeg','image/png'], 22 | maxSize: 2*1024*1024, 23 | cropRatio: 4/3, 24 | imageDim: [400, 300], 25 | cropForce: true, 26 | multiple: false 27 | }, 28 | function(blob){ 29 | $('[name=image]').val(blob.url); 30 | $('.image-thumbnail').attr('src', blob.url); 31 | }); 32 | }, 33 | 34 | 'submit form': function (e) { 35 | e.preventDefault(); 36 | var currentPostId = this._id; 37 | 38 | // place input values in post variable 39 | var postProperties = { 40 | title: $(e.target).find('[name=title]').val(), 41 | link: $(e.target).find('[name=link]').val(), 42 | description: $(e.target).find('[name=description]').val(), 43 | color: $(e.target).find('[name=color]').val(), 44 | image: $(e.target).find('[name=image]').val() 45 | }; 46 | 47 | Meteor.call('editPost', currentPostId, postProperties, function(error, result) { 48 | // abort and alert error reason 49 | if (error) { 50 | return sweetAlert(error.reason); 51 | } else { 52 | // if successfully updated, go to homepage 53 | sweetAlert({ 54 | title: "Your updates were saved!", 55 | type: "success", 56 | confirmButtonText: "Back to homepage" 57 | }, function() { 58 | Router.go('home'); 59 | }); 60 | // track event with segment 61 | analytics.track('Edited Post'); 62 | } 63 | }); 64 | }, 65 | 66 | 'click .js-delete': function(e) { 67 | e.preventDefault(); 68 | var currentPostId = this._id; 69 | sweetAlert({ 70 | title: "Are you sure?", 71 | type: "warning", 72 | showCancelButton: true, 73 | confirmButtonText: "Yes, delete it!", 74 | closeOnConfirm: false 75 | }, function(isConfirm) { 76 | if (isConfirm) { 77 | Meteor.call('deletePost', currentPostId, function(error, result) { 78 | // abort and alert error reason 79 | if (error) { 80 | return sweetAlert(error.reason); 81 | } else { 82 | // go to homepage when finished 83 | sweetAlert({ 84 | title: "Successfully deleted!", 85 | type: "success", 86 | confirmButtonText: "Back to homepage" 87 | }, function() { 88 | Router.go('home'); 89 | }); 90 | // track event with segment 91 | analytics.track('Deleted Post'); 92 | } 93 | }); 94 | } 95 | }); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | ## Stylesheets is a community-generated collection of the best CSS resources. 4 | 5 | ## It was created to give the web design community a place to share and catalog all the best tools, tutorials, snippets, and repos you can find. 6 | You can post anything that's CSS-related and let the community “star” something to let others know they found it useful, and also add it to their personal collection of starred posts. By default, posts are sorted showing the newest ones first, but you can also sort by number of stars and alphabetically. 7 | 8 | For those interested in some of the my other work, be sure to check out [jamiewilson.io](http://jamiewilson.io">jamiewilson.io). 9 | 10 | ## Special thanks to [ScreenshotMachine](http://screenshotmachine.com) 11 | I'd like to thank ScreenshotMachine for generously donating their screenshot generation service. Their service is really quick, easy and reliable. If you're in the market for a service for generating screenshots, then definitely give [ScreenshotMachine](http://screenshotmachine.com) a try. 12 | 13 | #### Other things I used to make this: 14 | - [Meteor](http://meteor.com) 15 | - [Discover Meteor](http://discovermeteor.com) 16 | - [Meteor Tips](http://meteortips.com/) 17 | - [Corpus CSS](http://corpuscss.com/) _Another side project_ 18 | - [Meteor Easy-Search](http://matteodem.github.io/meteor-easy-search/) 19 | - [Sweet Alert](http://t4t5.github.io/sweetalert/) 20 | - [Filepicker](https://www.filepicker.com/) 21 | - [Mongol](https://github.com/msavin/Mongol/) 22 | - [Meteor Up](https://github.com/arunoda/meteor-up) 23 | - [Digital Ocean](http://www.digitalocean.com/?refcode=f62fe98759a8) _$10 discount link_ 24 | - [Houston](https://github.com/gterrono/houston) 25 | - [Mailgun](https://mailgun.com/) 26 | - [Segment](https://segment.com/) 27 | - [Intercom](https://www.intercom.io/) 28 | 29 | *Thank you!* 30 | 31 | ### The MIT License (MIT) 32 | Copyright (c) 2015 Jamie Wilson 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy 35 | of this software and associated documentation files (the "Software"), to deal 36 | in the Software without restriction, including without limitation the rights 37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | copies of the Software, and to permit persons to whom the Software is 39 | furnished to do so, subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in 42 | all copies or substantial portions of the Software.

43 | 44 | The software is provided "as is", without warranty of any kind, express or 45 | implied, including but not limited to the warranties of merchantability, 46 | fitness for a particular purpose and noninfringement. In no event shall the 47 | authors or copyright holders be liable for any claim, damages or other 48 | liability, whether in an action of contract, tort or otherwise, arising from, 49 | out of or in connection with the software or the use or other dealings in 50 | the software. 51 | 52 | **More information on use and contributions coming soon.** 53 | -------------------------------------------------------------------------------- /public/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/embedly.js: -------------------------------------------------------------------------------- 1 | fillEmbedlyData = function() { 2 | // Fields to fill in 3 | var linkField = $('[name=link]'); 4 | var titleField = $('[name=title]'); 5 | var descriptionField = $('[name=description]'); 6 | var colorField = $('[name=color]'); 7 | var url = $('[name=link]').val(); 8 | 9 | // Convert an RBG value to string 10 | function componentToHex(c) { 11 | var hex = c.toString(16); 12 | return hex.length == 1 ? "0" + hex : hex; 13 | } 14 | 15 | function rgbToHex(r, g, b) { 16 | return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); 17 | } 18 | 19 | // Only get embedly data if url has been provided 20 | if (url) { 21 | 22 | // show that embedly is loading data 23 | titleField.attr('placeholder', 'Fetching website info…'); 24 | descriptionField.attr('placeholder', 'Fetching website info…'); 25 | 26 | Meteor.call('getEmbedlyData', url, function (error, data) { 27 | if (error) { 28 | sweetAlert(error.reason); 29 | } 30 | if (data) { 31 | // console.log(data); 32 | 33 | // fill link field with returned URL 34 | linkField.val(data.url); 35 | 36 | // generate hash for screenshotmachine api request 37 | var screenshotSrc = function() { 38 | url = data.url; 39 | secret = Meteor.settings.public.screenshotMachineSecret; 40 | var hash = CryptoJS.MD5(url + secret).toString(); 41 | // change button text if image source is updated 42 | $('.filepicker').text('Replace Image'); 43 | return 'https://api.screenshotmachine.com/?key=0151a3&size=T&hash=' + hash + '&url=' + url; 44 | } 45 | // set source of image 46 | $('.image-thumbnail').attr('src', screenshotSrc()); 47 | 48 | // If there isn't already a title, set returned title 49 | if (!titleField.val()) { 50 | titleField.val(data.title); 51 | } 52 | 53 | // If there isn't already a description, set returned description 54 | if (!descriptionField.val()) { 55 | descriptionField.val(data.description); 56 | } 57 | 58 | // Convert favicon_colors to hex and set input 59 | if (data.favicon_colors) { 60 | var rgbValue = data.favicon_colors[0]; 61 | var r = rgbValue.color[0]; 62 | var g = rgbValue.color[1]; 63 | var b = rgbValue.color[2]; 64 | var hexColor = rgbToHex(r, g, b); 65 | colorField.val(hexColor); 66 | } 67 | 68 | // if title, truncate it 69 | if (data.title) { 70 | // lib/counters.js 71 | truncateTitle(); 72 | // if not, set placeholder back to default 73 | } else { 74 | titleField.attr('placeholder', 'Name your post'); 75 | } 76 | 77 | // if description, truncate it 78 | if (data.description) { 79 | // lib/counters.js 80 | truncateDescription(); 81 | // if not, set placeholder back to default 82 | } else { 83 | descriptionField.attr('placeholder', 'Short description'); 84 | } 85 | // if no data is returned, set placeholder back to default 86 | } else { 87 | titleField.attr('placeholder', 'Name your post'); 88 | descriptionField.attr('placeholder', 'Short description'); 89 | } 90 | }); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /public/img/waffle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/fixtures.js: -------------------------------------------------------------------------------- 1 | if (Posts.find().count() === 0) { 2 | var now = new Date().getTime(); 3 | var step = 1; 4 | Posts.insert({ 5 | title: "CSS Reference | Codrops", 6 | link: "http://tympanus.net/codrops/css_reference/", 7 | description: "An extensive CSS reference with all the important properties and info to learn CSS from the basics", 8 | createdBy: 'Tg59AXAxYGeJxNc7f', 9 | dateCreated: new Date(now - step++ * 3600 * 1000), 10 | upvoters: [], 11 | stars: 0, 12 | color: "#007fb3" 13 | }); 14 | Posts.insert({ 15 | title: "CriticalCSS", 16 | link: "https://github.com/filamentgroup/criticalcss", 17 | description: "Finds the Above the Fold CSS for your page, and outputs it into a file", 18 | createdBy: '', 19 | dateCreated: new Date(now - step++ * 3600 * 1000), 20 | upvoters: [], 21 | stars: 0, 22 | color: "#ffa300" 23 | }); 24 | Posts.insert({ 25 | title: "CSS polyfills from the future | GSS", 26 | link: "http://gridstylesheets.org/", 27 | description: "GSS reimagines CSS layout by using the Cassowary Constraint Solver algorithm", 28 | createdBy: 'Tg59AXAxYGeJxNc7f', 29 | dateCreated: new Date(now - step++ * 3600 * 1000), 30 | upvoters: [], 31 | stars: 0, 32 | color: "#002034" 33 | }); 34 | Posts.insert({ 35 | title: "Basscss", 36 | link: "http://www.basscss.com/", 37 | description: "Lightning-Fast Modular CSS with No Side Effects", 38 | createdBy: 'Tg59AXAxYGeJxNc7f', 39 | dateCreated: new Date(now - step++ * 3600 * 1000), 40 | upvoters: [], 41 | stars: 1, 42 | color: "#0070dc" 43 | }); 44 | Posts.insert({ 45 | title: "ungrid", 46 | link: "http://chrisnager.github.io/ungrid/", 47 | description: "The simplest responsive css grid", 48 | createdBy: '', 49 | dateCreated: new Date(now - step++ * 3600 * 1000), 50 | upvoters: [], 51 | stars: 1, 52 | color: "#c9cbae" 53 | }); 54 | Posts.insert({ 55 | title: "What the Flexbox?", 56 | link: "http://flexbox.io", 57 | description: "A simple, free 20 video course that will help you master CSS Flexbox", 58 | createdBy: 'Tg59AXAxYGeJxNc7f', 59 | dateCreated: new Date(now - step++ * 3600 * 1000), 60 | upvoters: [], 61 | stars: 0, 62 | color: "#9f6acc" 63 | }); 64 | Posts.insert({ 65 | title: "BADA55.io - CSS hex color words for web developers", 66 | link: "http://bada55.io", 67 | description: "Finding the most badass leet words for your CSS hex colors", 68 | createdBy: '', 69 | dateCreated: new Date(now - step++ * 3600 * 1000), 70 | upvoters: [], 71 | stars: 0, 72 | color: "#ffd584" 73 | }); 74 | Posts.insert({ 75 | title: "Myth - CSS the way it was imagined.", 76 | link: "http://www.myth.io/", 77 | description: "CSS the way it was imagined", 78 | createdBy: 'Tg59AXAxYGeJxNc7f', 79 | dateCreated: new Date(now - step++ * 3600 * 1000), 80 | upvoters: [], 81 | stars: 0, 82 | color: "#797bd8" 83 | }); 84 | Posts.insert({ 85 | title: "Skeleton: Responsive CSS Boilerplate", 86 | link: "http://getskeleton.com/", 87 | description: "A dead simple, responsive boilerplate.", 88 | createdBy: '', 89 | dateCreated: new Date(now - step++ * 3600 * 1000), 90 | upvoters: [], 91 | stars: 0, 92 | color: "#1c1c1c" 93 | }); 94 | Posts.insert({ 95 | title: "CSSCV", 96 | link: "https://github.com/csswizardry/csscv", 97 | description: "A simple, opinionated stylesheet for formatting semantic HTML to look like a CSS file.", 98 | createdBy: 'Tg59AXAxYGeJxNc7f', 99 | dateCreated: new Date(now - step++ * 3600 * 1000), 100 | upvoters: [], 101 | stars: 0, 102 | color: "#002936" 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /public/img/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/slack.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/methods.js: -------------------------------------------------------------------------------- 1 | getEmbedlyData = function(url) { 2 | var data = {}; 3 | var extractBase = 'http://api.embed.ly/1/extract'; 4 | var embedlyKey = Meteor.settings.public.embedlyApiKey; 5 | 6 | try { 7 | var result = Meteor.http.get(extractBase, { 8 | params: { 9 | key: embedlyKey, 10 | url: url 11 | } 12 | }); 13 | return _.pick(result.data, 'url', 'title', 'description', 'favicon_colors'); 14 | 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | } 19 | 20 | Meteor.methods({ 21 | sendVerificationEmail: function(userId) { 22 | Accounts.sendVerificationEmail(userId); 23 | }, 24 | getEmbedlyData: function(url) { 25 | check(url, String); 26 | return getEmbedlyData(url); 27 | }, 28 | submitPost: function(postProperties) { 29 | // make sure user is actually signed in 30 | check(Meteor.userId(), String); 31 | // check input values to be correctly formatted 32 | check(postProperties, { 33 | link: String, 34 | title: String, 35 | description: String, 36 | color: String, 37 | image: String 38 | }); 39 | 40 | // if link has already been posted, send to that post 41 | var alreadyPosted = Posts.findOne({link: postProperties.link}); 42 | if (alreadyPosted) { 43 | return { 44 | postExists: true, 45 | _id: alreadyPosted._id 46 | } 47 | } 48 | 49 | var user = Meteor.user(); 50 | // add user id and date to post 51 | var postComplete = _.extend(postProperties, { 52 | createdBy: user._id, 53 | dateCreated: new Date(), 54 | upvoters: [], 55 | stars: 0 56 | }); 57 | // add post to posts collection 58 | var postId = Posts.insert(postComplete); 59 | return { 60 | _id: postId 61 | }; 62 | }, 63 | 64 | postTweet: function(postProperties) { 65 | Twitter = new TwitMaker({ 66 | consumer_key: Meteor.settings.twitter.consumerKey, 67 | consumer_secret: Meteor.settings.twitter.consumerSecret, 68 | access_token: Meteor.settings.twitter.accessToken, 69 | access_token_secret: Meteor.settings.twitter.accessTokenSecret 70 | }); 71 | 72 | var tweetContent = postProperties.title + ": " + postProperties.link + " #CSS"; 73 | Twitter.post('statuses/update', { 74 | status: tweetContent 75 | }, function(error) { 76 | if (error) 77 | return console.log(error); 78 | }); 79 | }, 80 | 81 | editPost: function (currentPostId, postProperties) { 82 | // only let owners update posts 83 | var currentUserId = Meteor.userId(); 84 | Posts.update({_id: currentPostId, createdBy: currentUserId}, {$set: postProperties}); 85 | }, 86 | 87 | deletePost: function (currentPostId) { 88 | // only let owners delete posts 89 | var currentUserId = Meteor.userId(); 90 | Posts.remove({_id: currentPostId, createdBy: currentUserId}); 91 | }, 92 | 93 | upvote: function(postId) { 94 | // check if user is logged in 95 | check(this.userId, String); 96 | // check if post exists 97 | check(postId, String); 98 | 99 | // check if user has upvoted already 100 | // add user to upvoters and increment votes by 1 101 | var affected = Posts.update({ 102 | _id: postId, 103 | upvoters: {$ne: this.userId} 104 | }, { 105 | $addToSet: {upvoters: this.userId}, 106 | $inc: {stars: 1} 107 | }); 108 | if (!affected) 109 | throw new Meteor.Error('invalid', "You weren't able to upvote that post"); 110 | }, 111 | downvote: function(postId) { 112 | // check if user is logged in 113 | check(this.userId, String); 114 | // check if post exists 115 | check(postId, String); 116 | 117 | // remove user from list of upvoters 118 | // add user to upvoters and increment votes by -1 119 | var affected = Posts.update({ 120 | _id: postId 121 | }, { 122 | $pull: {upvoters: this.userId}, 123 | $inc: {stars: -1} 124 | }); 125 | if (!affected) 126 | throw new Meteor.Error('invalid', "You weren't able to downvote that post"); 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /client/templates/about/about.html: -------------------------------------------------------------------------------- 1 | 60 | -------------------------------------------------------------------------------- /client/posts/simply_countable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Simply Countable plugin 3 | * Provides a character counter for any text input or textarea 4 | * 5 | * @version 0.4.2 6 | * @homepage http://github.com/aaronrussell/jquery-simply-countable/ 7 | * @author Aaron Russell (http://www.aaronrussell.co.uk) 8 | * 9 | * Copyright (c) 2009-2010 Aaron Russell (aaron@gc4.co.uk) 10 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 11 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 12 | */ 13 | 14 | (function($){ 15 | 16 | $.fn.simplyCountable = function(options){ 17 | 18 | options = $.extend({ 19 | counter: '#counter', 20 | countType: 'characters', 21 | maxCount: 140, 22 | strictMax: false, 23 | countDirection: 'down', 24 | safeClass: 'safe', 25 | overClass: 'over', 26 | thousandSeparator: ',', 27 | onOverCount: function(){}, 28 | onSafeCount: function(){}, 29 | onMaxCount: function(){} 30 | }, options); 31 | 32 | var navKeys = [33,34,35,36,37,38,39,40]; 33 | 34 | return $(this).each(function(){ 35 | 36 | var countable = $(this); 37 | var counter = $(options.counter); 38 | if (!counter.length) { return false; } 39 | 40 | var countCheck = function(){ 41 | 42 | var count; 43 | var revCount; 44 | 45 | var reverseCount = function(ct){ 46 | return ct - (ct*2) + options.maxCount; 47 | } 48 | 49 | var countInt = function(){ 50 | return (options.countDirection === 'up') ? revCount : count; 51 | } 52 | 53 | var numberFormat = function(ct){ 54 | var prefix = ''; 55 | if (options.thousandSeparator){ 56 | ct = ct.toString(); 57 | // Handle large negative numbers 58 | if (ct.match(/^-/)) { 59 | ct = ct.substr(1); 60 | prefix = '-'; 61 | } 62 | for (var i = ct.length-3; i > 0; i -= 3){ 63 | ct = ct.substr(0,i) + options.thousandSeparator + ct.substr(i); 64 | } 65 | } 66 | return prefix + ct; 67 | } 68 | 69 | var changeCountableValue = function(val){ 70 | countable.val(val).trigger('change'); 71 | } 72 | 73 | /* Calculates count for either words or characters */ 74 | if (options.countType === 'words'){ 75 | count = options.maxCount - $.trim(countable.val()).split(/\s+/).length; 76 | if (countable.val() === ''){ count += 1; } 77 | } 78 | else { count = options.maxCount - countable.val().length; } 79 | revCount = reverseCount(count); 80 | 81 | /* If strictMax set restrict further characters */ 82 | if (options.strictMax && count <= 0){ 83 | var content = countable.val(); 84 | if (count < 0) { 85 | options.onMaxCount(countInt(), countable, counter); 86 | } 87 | if (options.countType === 'words'){ 88 | var allowedText = content.match( new RegExp('\\s?(\\S+\\s+){'+ options.maxCount +'}') ); 89 | if (allowedText) { 90 | changeCountableValue(allowedText[0]); 91 | } 92 | } 93 | else { changeCountableValue(content.substring(0, options.maxCount)); } 94 | count = 0, revCount = options.maxCount; 95 | } 96 | 97 | counter.text(numberFormat(countInt())); 98 | 99 | /* Set CSS class rules and API callbacks */ 100 | if (!counter.hasClass(options.safeClass) && !counter.hasClass(options.overClass)){ 101 | if (count < 0){ counter.addClass(options.overClass); } 102 | else { counter.addClass(options.safeClass); } 103 | } 104 | else if (count < 0 && counter.hasClass(options.safeClass)){ 105 | counter.removeClass(options.safeClass).addClass(options.overClass); 106 | options.onOverCount(countInt(), countable, counter); 107 | } 108 | else if (count >= 0 && counter.hasClass(options.overClass)){ 109 | counter.removeClass(options.overClass).addClass(options.safeClass); 110 | options.onSafeCount(countInt(), countable, counter); 111 | } 112 | 113 | }; 114 | 115 | countCheck(); 116 | 117 | countable.on('keyup blur paste', function(e) { 118 | switch(e.type) { 119 | case 'keyup': 120 | // Skip navigational key presses 121 | if ($.inArray(e.which, navKeys) < 0) { countCheck(); } 122 | break; 123 | case 'paste': 124 | // Wait a few miliseconds if a paste event 125 | setTimeout(countCheck, (e.type === 'paste' ? 5 : 0)); 126 | break; 127 | default: 128 | countCheck(); 129 | break; 130 | } 131 | }); 132 | 133 | }); 134 | 135 | }; 136 | 137 | })(jQuery); 138 | -------------------------------------------------------------------------------- /client/templates/app/terms.html: -------------------------------------------------------------------------------- 1 | 84 | --------------------------------------------------------------------------------