diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..a167df14d6 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,220 @@ +{ + "ecmaFeatures": { + "jsx": true + }, + "env": { + "browser": true, + "mocha": true, + "node": true + }, + "globals": { + "window": true, + "$": true, + "ga": true, + "jQuery": true + }, + "rules": { + "no-comma-dangle": 2, + "no-cond-assign": 2, + "no-console": 0, + "no-constant-condition": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-dupe-keys": 2, + "no-empty": 2, + "no-empty-class": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "valid-jsdoc": 2, + "valid-typeof": 2, + + "block-scoped-var": 2, + "complexity": 0, + "consistent-return": 2, + "curly": 2, + "default-case": 1, + "dot-notation": 0, + "eqeqeq": 1, + "guard-for-in": 1, + "no-alert": 1, + "no-caller": 2, + "no-div-regex": 2, + "no-else-return": 0, + "no-empty-label": 2, + "no-eq-null": 1, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 1, + "no-multi-spaces": 1, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-process-env": 0, + "no-proto": 2, + "no-redeclare": 1, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-unused-expressions": 2, + "no-void": 1, + "no-warning-comments": [ + 1, + { + "terms": [ + "fixme" + ], + "location": "start" + } + ], + "no-with": 2, + "radix": 2, + "vars-on-top": 0, + "wrap-iife": [2, "any"], + "yoda": 0, + + "strict": 0, + + "no-catch-shadow": 2, + "no-delete-var": 2, + "no-label-var": 2, + "no-shadow": 0, + "no-shadow-restricted-names": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 1, + "no-unused-vars": 2, + "no-use-before-define": 0, + + "handle-callback-err": 2, + "no-mixed-requires": 0, + "no-new-require": 2, + "no-path-concat": 2, + "no-process-exit": 2, + "no-restricted-modules": 0, + "no-sync": 0, + + "brace-style": [ + 2, + "1tbs", + { "allowSingleLine": true } + ], + "camelcase": 1, + "comma-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "comma-style": [ + 2, "last" + ], + "consistent-this": 0, + "eol-last": 2, + "func-names": 0, + "func-style": 0, + "key-spacing": [ + 2, + { + "beforeColon": false, + "afterColon": true + } + ], + "max-nested-callbacks": 0, + "new-cap": 0, + "new-parens": 2, + "no-array-constructor": 2, + "no-inline-comments": 1, + "no-lonely-if": 1, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [ + 1, + { "max": 2 } + ], + "no-nested-ternary": 2, + "no-new-object": 2, + "no-space-before-semi": 2, + "no-spaced-func": 2, + "no-ternary": 0, + "no-trailing-spaces": 1, + "no-underscore-dangle": 0, + "no-wrap-func": 2, + "one-var": 0, + "operator-assignment": 0, + "padded-blocks": 0, + "quote-props": 0, + "quotes": [ + 2, + "single", + "avoid-escape" + ], + "semi": [ + 2, + "always" + ], + "sort-vars": 0, + "space-after-keywords": [ + 2, + "always", + { "checkFunctionKeyword": false } + ], + "space-after-function-names": "never", + "space-before-blocks": [ + 2, + "always" + ], + "space-in-brackets": 0, + "space-in-parens": 0, + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-ops": [ + 1, + { + "words": true, + "nonwords": false + } + ], + "spaced-line-comment": [ + 2, + "always", + { "exceptions": ["-"] } + ], + "wrap-regex": 1, + + "max-depth": 0, + "max-len": [ + 1, + 80, + 2 + ], + "max-params": 0, + "max-statements": 0, + "no-bitwise": 1, + "no-plusplus": 0 + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 002acaf376..01824e83b9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ node_modules .DS_Store Thumbs.db bower_components +.eslintignore +.eslintrc diff --git a/app.js b/app.js index 5fad3f089a..c88fd0b5e3 100644 --- a/app.js +++ b/app.js @@ -34,6 +34,11 @@ var express = require('express'), bonfireController = require('./controllers/bonfire'), coursewareController = require('./controllers/courseware'), + /** + * Stories + */ + storyController = require('./controllers/story'); + /** * User model */ @@ -219,6 +224,7 @@ app.get('/chat', resourcesController.chat); app.get('/live-pair-programming', resourcesController.livePairProgramming); app.get('/install-screenhero', resourcesController.installScreenHero); app.get('/javascript-in-your-inbox', resourcesController.javaScriptInYourInbox); +app.get('/guide-to-our-nonprofit-projects', resourcesController.guideToOurNonprofitProjects); app.get('/chromebook', resourcesController.chromebook); app.get('/deploy-a-website', resourcesController.deployAWebsite); app.get('/gmail-shortcuts', resourcesController.gmailShortcuts); @@ -272,6 +278,92 @@ app.post( userController.updateProgress ); +/** + * Main routes. + */ +app.get( + '/stories/hotStories', + storyController.hotJSON +); + +app.get( + '/stories/recentStories', + storyController.recentJSON +); + +app.get( + '/stories/', + function(req, res) { + res.redirect(302, '/stories/hot'); + } +); + +app.get( + '/stories/comments/:id', + storyController.comments +); + +app.post( + '/stories/comment/', + storyController.commentSubmit +); + +app.post( + '/stories/comment/:id/comment', + storyController.commentOnCommentSubmit +); + +app.get( + '/stories/submit', + storyController.submitNew +); + +app.get( + '/stories/submit/:newStory', + storyController.preSubmit +); + +app.post( + '/stories/preliminary', + storyController.newStory +); + +app.post( + '/stories/', + storyController.storySubmission +); + +app.get( + '/stories/hot', + storyController.hot +); + +app.get( + '/stories/recent', + storyController.recent +); + + +app.get( + '/stories/search', + storyController.search +); + +app.post( + '/stories/search', + storyController.getStories +); + +app.get( + '/stories/:storyName', + storyController.returnIndividualStory +); + +app.post( + '/stories/upvote/', + storyController.upvote +); + /** * Challenge related routes */ diff --git a/controllers/resources.js b/controllers/resources.js index 1ce4d9af82..22efac5bb4 100644 --- a/controllers/resources.js +++ b/controllers/resources.js @@ -1,6 +1,8 @@ var User = require('../models/User'), Challenge = require('./../models/Challenge'), Bonfire = require('./../models/Bonfire'), + Story = require('./../models/Story'), + Comment = require('./../models/Comment'), resources = require('./resources.json'), questions = resources.questions, steps = resources.steps, @@ -8,9 +10,11 @@ var User = require('../models/User'), bonfires = require('../seed_data/bonfires.json'), coursewares = require('../seed_data/coursewares.json'), moment = require('moment'), - Client = require('node-rest-client').Client, - client = new Client(), - debug = require('debug')('freecc:cntr:bonfires'); + https = require('https'), + debug = require('debug')('freecc:cntr:resources'), + cheerio = require('cheerio'), + request = require('request'), + R = require('ramda'); /** * GET / @@ -24,33 +28,6 @@ module.exports = { }); }, - stats: function stats(req, res) { - var date1 = new Date("10/15/2014"); - var date2 = new Date(); - var timeDiff = Math.abs(date2.getTime() - date1.getTime()); - var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24)); - client.get('https://trello.com/1/boards/BA3xVpz9/cards?key=' + secrets.trello.key, function(trello, response) { - var nonprofitProjects = (trello && trello.length) || 15; - User.count({'points': {'$gt': 2}}, function(err, c3) { if (err) { debug('User err: ', err); next(err); } - User.count({'points': {'$gt': 9}}, function(err, c10) { if (err) { debug('User err: ', err); next(err); } - User.count({'points': {'$gt': 29}}, function(err, c30) { if (err) { debug('User err: ', err); next(err); } - User.count({'points': {'$gt': 53}}, function(err, all) { if (err) { debug('User err: ', err); next(err); } - res.render('resources/stats', { - title: 'Free Code Camp Stats:', - daysRunning: daysRunning, - nonprofitProjects: nonprofitProjects, - c3: c3, - c10: c10, - c30: c30, - all: all - }); - }); - }); - }); - }); - }); - }, - sitemap: function sitemap(req, res, next) { var appUrl = 'http://www.freecodecamp.com'; var now = moment(new Date).format('YYYY-MM-DD'); @@ -109,6 +86,12 @@ module.exports = { }); }, + guideToOurNonprofitProjects: function guideToOurNonprofitProjects(req, res) { + res.render('resources/guide-to-our-nonprofit-projects', { + title: 'A guide to our Nonprofit Projects' + }); + }, + controlShortcuts: function controlShortcuts(req, res) { res.render('resources/control-shortcuts', { title: 'These Control Shortcuts will save you Hours' @@ -146,22 +129,27 @@ module.exports = { }, githubCalls: function(req, res) { var githubHeaders = {headers: {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36'}, port:80 }; - client.get('https://api.github.com/repos/freecodecamp/freecodecamp/pulls?client_id=' + secrets.github.clientID + '&client_secret=' + secrets.github.clientSecret, githubHeaders, function(pulls, res3) { + request('https://api.github.com/repos/freecodecamp/freecodecamp/pulls?client_id=' + secrets.github.clientID + '&client_secret=' + secrets.github.clientSecret, githubHeaders, function(err, status1, pulls) { pulls = pulls ? Object.keys(JSON.parse(pulls)).length : "Can't connect to github"; - client.get('https://api.github.com/repos/freecodecamp/freecodecamp/issues?client_id=' + secrets.github.clientID + '&client_secret=' + secrets.github.clientSecret, githubHeaders, function (issues, res4) { + debug('pulls', pulls); + request('https://api.github.com/repos/freecodecamp/freecodecamp/issues?client_id=' + secrets.github.clientID + '&client_secret=' + secrets.github.clientSecret, githubHeaders, function (err, status2, issues) { + debug('issues', issues); issues = ((pulls === parseInt(pulls)) && issues) ? Object.keys(JSON.parse(issues)).length - pulls : "Can't connect to GitHub"; res.send({"issues": issues, "pulls" : pulls}); }); }); }, + + + trelloCalls: function(req, res) { - client.get('https://trello.com/1/boards/BA3xVpz9/cards?key=' + secrets.trello.key, function(trello, res2) { + request('https://trello.com/1/boards/BA3xVpz9/cards?key=' + secrets.trello.key, function(err, status, trello) { trello = trello ? (JSON.parse(trello)).length : "Can't connect to to Trello"; res.send({"trello": trello}); }); }, bloggerCalls: function(req, res) { - client.get('https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/posts?key=' + secrets.blogger.key, function (blog, res5) { + request('https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/posts?key=' + secrets.blogger.key, function (err, status, blog) { var blog = blog.length > 100 ? JSON.parse(blog) : ""; res.send({ blog1Title: blog ? blog["items"][0]["title"] : "Can't connect to Blogger", @@ -262,7 +250,7 @@ module.exports = { return { _id: elem._id, difficulty: elem.difficulty - } + }; }) .sort(function(a, b) { return a.difficulty - b.difficulty; @@ -276,7 +264,7 @@ module.exports = { return { name: elem.name, difficulty: elem.difficulty - } + }; }) .sort(function(a, b) { return a.difficulty - b.difficulty; @@ -287,9 +275,64 @@ module.exports = { }, whichEnvironment: function() { return process.env.NODE_ENV; + }, + getURLTitle: function(url, callback) { + + (function () { + var result = {title: ''}; + request(url, function (error, response, body) { + if (!error && response.statusCode === 200) { + var $ = cheerio.load(body); + var title = $('title').text(); + result.title = title; + debug('calling callback with', result); + callback(null, result); + } else { + callback('failed'); + } + }); + })(); + }, + updateUserStoryPictures: function(userId, picture) { + + var counter = 0, + foundStories, + foundComments; + + Story.find({'author.userId': userId}, function(err, stories) { + if (err) { + throw err; + } + foundStories = stories; + counter++; + saveStoriesAndComments(); + }); + Comment.find({'author.userId': userId}, function(err, comments) { + if (err) { + throw err; + } + foundComments = comments; + counter++; + saveStoriesAndComments(); + }); + + function saveStoriesAndComments() { + if (counter !== 2) { + return; + } + R.forEach(function(comment) { + comment.author.picture = picture; + comment.markModified('author'); + comment.save(); + }, foundComments); + + R.forEach(function(story) { + story.author.picture = picture; + debug('This is a story', story); + debug(story.author.picture); + story.markModified('author'); + story.save(); + }, foundStories); + } } - }; - - - diff --git a/controllers/story.js b/controllers/story.js new file mode 100644 index 0000000000..78ceda95d4 --- /dev/null +++ b/controllers/story.js @@ -0,0 +1,347 @@ +var R = require('ramda'), + debug = require('debug')('freecc:cntr:story'), + Story = require('./../models/Story'), + Comment = require('./../models/Comment'), + User = require('./../models/User'), + moment = require('../public/js/lib/moment/moment.js'), + resources = require('./resources'), + mongodb = require('mongodb'), + MongoClient = mongodb.MongoClient, + secrets = require('../config/secrets'), + User = require('./../models/User'); + +function hotRank(timeValue, rank, headline) { + /* + * Hotness ranking algorithm: http://amix.dk/blog/post/19588 + * tMS = postedOnDate - foundationTime; + * Ranking... + * f(ts, 1, rank) = log(10)z + (ts)/45000; + */ + var hotness; + var z = Math.log(rank) / Math.log(10); + hotness = z + (timeValue / 45000000); + return hotness; + +} + +exports.hotJSON = function(req, res, next) { + var story = Story.find({}).sort({'timePosted': -1}).limit(1000); + story.exec(function(err, stories) { + if (err) { + throw err; + } + + var foundationDate = 1413298800000; + + var sliceVal = stories.length >= 100 ? 100 : stories.length; + var rankedStories = stories; + return res.json(rankedStories.map(function(elem) { + return elem; + }).sort(function(a, b) { + debug('a rank and b rank', hotRank(a.timePosted - foundationDate, a.rank, a.headline), hotRank(b.timePosted - foundationDate, b.rank, b.headline)); + return hotRank(b.timePosted - foundationDate, b.rank, b.headline) - hotRank(a.timePosted - foundationDate, a.rank, a.headline); + }).slice(0, sliceVal)); + + }); +}; + +exports.recentJSON = function(req, res, next) { + var story = Story.find({}).sort({'timePosted': -1}).limit(100); + story.exec(function(err, stories) { + if (err) { + throw err; + } + res.json(stories); + }); +}; + +exports.hot = function(req, res, next) { + res.render('stories/index', { + page: 'hot' + }); +}; + +exports.submitNew = function(req,res, next) { + res.render('stories/index', { + page: 'submit' + }); +}; + +exports.search = function(req, res, next) { + res.render('stories/index', { + page: 'search' + }); +}; + +exports.recent = function(req, res, next) { + res.render('stories/index', { + page: 'recent' + }); +}; + +exports.preSubmit = function(req, res, next) { + + var data = req.params.newStory; + + + data = data.replace(/url=/gi, '').replace(/&title=/gi, ',').split(','); + var url = data[0]; + var title = data[1]; + res.render('stories/index', { + page: 'storySubmission', + storyURL: url, + storyTitle: title + }); +}; + + +exports.returnIndividualStory = function(req, res, next) { + var dashedName = req.params.storyName; + + var storyName = dashedName.replace(/\-/g, ' '); + + Story.find({'storyLink' : new RegExp(storyName, 'i')}, function(err, story) { + if (err) { + next(err); + } + + + if (story.length < 1) { + req.flash('errors', { + msg: "404: We couldn't find a story with that name. Please double check the name." + }); + + return res.redirect('/stories/'); + } + + story = story.pop(); + var dashedNameFull = story.storyLink.toLowerCase().replace(/\s/g, '-'); + if (dashedNameFull !== dashedName) { + return res.redirect('../stories/' + dashedNameFull); + } + + res.render('stories/index', { + title: story.headline, + link: story.link, + author: story.author, + description: story.description, + rank: story.upVotes.length, + upVotes: story.upVotes, + comments: story.comments, + id: story._id, + user: req.user, + timeAgo: moment(story.timePosted).fromNow(), + image: story.image, + page: 'show' + }); + }); +}; + +exports.getStories = function(req, res, next) { + MongoClient.connect(secrets.db, function(err, database) { + var db = database; + db.collection('stories').find({ + "$text": { + "$search": req.body.data.searchValue + } + }, { + headline: 1, + timePosted: 1, + link: 1, + description: 1, + rank: 1, + upVotes: 1, + author: 1, + comments: 1, + image: 1, + storyLink: 1, + textScore: { + $meta: "textScore" + } + }, { + sort: { + textScore: { + $meta: "textScore" + } + } + }).toArray(function(err, items) { + if (items !== null && items.length !== 0) { + return res.json(items); + } + return res.status(404); + }); + }); +}; + +exports.upvote = function(req, res, next) { + var data = req.body.data; + Story.find({'_id': data.id}, function(err, story) { + if (err) { + throw err; + } + story = story.pop(); + story.rank++; + story.upVotes.push( + { + upVotedBy: data.upVoter._id, + upVotedByUsername: data.upVoter.profile.username + } + ); + story.markModified('rank'); + story.save(); + return res.send(story); + }); +}; + +exports.comments = function(req, res, next) { + var data = req.params.id; + Comment.find({'_id': data}, function(err, comment) { + if (err) { + throw err; + } + comment = comment.pop(); + return res.send(comment); + }); +}; + +exports.newStory = function(req, res, next) { + var url = req.body.data.url; + if (url.search(/^https?:\/\//g) === -1) { + url = 'http://' + url; + } + debug('In pre submit with a url', url); + + Story.find({'link': url}, function(err, story) { + debug('Attempting to find a story'); + if (err) { + debug('oops'); + return res.status(500); + } + if (story.length) { + debug('Found a story already, here\'s the return from find', story); + req.flash('errors', { + msg: "Someone's already posted that link. Here's the discussion." + }); + debug('Redirecting the user with', story[0].storyLink); + return res.json({ + alreadyPosted: true, + storyURL: story.pop().storyLink + }); + } + resources.getURLTitle(url, processResponse); + }); + + function processResponse(err, storyTitle) { + if (err) { + res.json({ + alreadyPosted: false, + storyURL: url, + storyTitle: '' + }); + } else { + storyTitle = storyTitle ? storyTitle : ''; + res.json({ + alreadyPosted: false, + storyURL: url, + storyTitle: storyTitle.title + }); + } + } +}; + +exports.storySubmission = function(req, res, next) { + var data = req.body.data; + var storyLink = data.headline + .replace(/\'/g, '') + .replace(/\"/g, '') + .replace(/,/g, '') + .replace(/[^a-z0-9]/gi, ' ') + .replace(/\s+/g, ' ') + .toLowerCase(); + var link = data.link; + if (link.search(/^https?:\/\//g) === -1) { + link = 'http://' + link; + } + var story = new Story({ + headline: data.headline, + timePosted: Date.now(), + link: link, + description: data.description, + rank: 1, + upVotes: data.upVotes, + author: data.author, + comments: [], + image: data.image, + storyLink: storyLink + }); + + story.save(function(err, data) { + if (err) { + return res.status(500); + } + res.send(JSON.stringify({ + storyLink: story.storyLink.replace(/\s/g, '-').toLowerCase() + })); + }); +}; + +exports.commentSubmit = function(req, res, next) { + debug('comment submit fired'); + var data = req.body.data; + var comment = new Comment({ + associatedPost: data.associatedPost, + body: data.body, + rank: 0, + upvotes: 0, + author: data.author, + comments: [], + topLevel: true, + commentOn: Date.now() + }); + commentSave(comment, Story, res); +}; + +exports.commentOnCommentSubmit = function(req, res, next) { + debug('comment on comment submit'); + var idToFind = req.params.id; + var data = req.body.data; + var comment = new Comment({ + associatedPost: data.associatedPost, + body: data.body, + rank: 0, + upvotes: 0, + author: data.author, + comments: [], + topLevel: false, + commentOn: Date.now() + }); + commentSave(comment, Comment, res); +}; + +function commentSave(comment, Context, res) { + comment.save(function(err, data) { + if (err) { + return res.status(500); + } + try { + Context.find({'_id': comment.associatedPost}, function (err, associatedStory) { + if (err) { + return res.status(500); + } + associatedStory = associatedStory.pop(); + if (associatedStory) { + associatedStory.comments.push(data._id); + associatedStory.save(function (err, data) { + if (err) { + res.status(500); + } + res.send(true); + }); + } + }); + } catch (e) { + // delete comment + return res.status(500); + } + }); +} diff --git a/controllers/user.js b/controllers/user.js index c7947d53a9..569f418e33 100644 --- a/controllers/user.js +++ b/controllers/user.js @@ -7,7 +7,8 @@ var _ = require('lodash'), secrets = require('../config/secrets'), moment = require('moment'), Challenge = require('./../models/Challenge'), - debug = require('debug')('freecc:cntr:challenges'); + debug = require('debug')('freecc:cntr:challenges') + resources = require('./resources'); //TODO(Berks): Refactor to use module.exports = {} pattern. @@ -314,7 +315,7 @@ exports.postUpdateProfile = function(req, res, next) { return next(err); } var user = req.user; - if (existingUsername && existingUsername.profile.username != user.profile.username) { + if (existingUsername && existingUsername.profile.username !== user.profile.username) { req.flash('errors', { msg: 'An account with that username already exists.' }); @@ -330,7 +331,7 @@ exports.postUpdateProfile = function(req, res, next) { user.profile.codepenProfile = req.body.codepenProfile.trim() || ''; user.profile.twitterHandle = req.body.twitterHandle.trim() || ''; user.profile.bio = req.body.bio.trim() || ''; - user.profile.picture = req.body.picture.trim() || ''; + user.profile.picture = req.body.picture.trim() || 'https://s3.amazonaws.com/freecodecamp/favicons/apple-touch-icon-180x180.png'; user.portfolio.website1Title = req.body.website1Title.trim() || ''; user.portfolio.website1Link = req.body.website1Link.trim() || ''; user.portfolio.website1Image = req.body.website1Image.trim() || ''; @@ -343,9 +344,12 @@ exports.postUpdateProfile = function(req, res, next) { user.save(function (err) { - if (err) return next(err); - req.flash('success', {msg: 'Profile information updated.'}); - res.redirect('/account'); + if (err) { + return next(err); + } + req.flash('success', {msg: 'Profile information updated.'}); + res.redirect('/account'); + resources.updateUserStoryPictures(user._id.toString(), user.profile.picture); }); }); }); diff --git a/models/Comment.js b/models/Comment.js new file mode 100644 index 0000000000..a3a35fe6fd --- /dev/null +++ b/models/Comment.js @@ -0,0 +1,39 @@ +var mongoose = require('mongoose'); +var secrets = require('../config/secrets'); + +var commentSchema = new mongoose.Schema({ + associatedPost: { + type: String, + required: true + }, + body: { + type: String, + default: '' + }, + rank: { + type: Number, + default: -Infinity + }, + upvotes: { + type: Array, + default: [] + }, + author: {}, + comments: { + type: Array, + default: [] + }, + commentOn: { + type: Number, + default: Date.now() + } +}); + +module.exports = mongoose.model('Comment', commentSchema); + +/* + author: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + */ \ No newline at end of file diff --git a/models/Story.js b/models/Story.js new file mode 100644 index 0000000000..76d95471a5 --- /dev/null +++ b/models/Story.js @@ -0,0 +1,58 @@ +var mongoose = require('mongoose'); +var secrets = require('../config/secrets'); + +var storySchema = new mongoose.Schema({ + headline: { + type: String, + unique: false + }, + timePosted: { + type: Number, + default: 0 + }, + link: { + type: String, + unique: false + }, + description: { + type: String, + unique: false + }, + rank: { + type: Number, + default: -Infinity + }, + upVotes: { + type: Array, + default: [] + }, + author: {}, + comments: { + type: Array, + default: [] + }, + image: { + type: String, + default: '' + }, + storyLink: { + type: String, + default: '' + } +}); + +storySchema.pre('save', function(next) { + console.log('pre save test'); + next(); +}); + +module.exports = mongoose.model('Story', storySchema); + +/* + author: { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + }, + */ diff --git a/package.json b/package.json index 2e0986284d..f44369004e 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,12 @@ "lusca": "^1.0.2", "method-override": "^2.3.0", "moment": "^2.8.4", + "mongodb": "^1.4.33", "mongoose": "^3.8.19", + "mongoose-text-search": "0.0.2", "morgan": "^1.5.0", "newrelic": "^1.13.3", "node": "0.0.0", - "node-rest-client": "^1.4.3", "nodemailer": "^1.3.0", "passport": "^0.2.1", "passport-facebook": "^1.0.3", @@ -57,7 +58,7 @@ "passport-oauth": "^1.0.0", "passport-twitter": "^1.0.2", "ramda": "^0.10.0", - "request": "^2.49.0", + "request": "^2.53.0", "sitemap": "^0.7.4", "uglify-js": "^2.4.15", "validator": "^3.22.1", diff --git a/public/css/main.less b/public/css/main.less index 9885b31d57..4af818a206 100644 --- a/public/css/main.less +++ b/public/css/main.less @@ -27,6 +27,8 @@ li, .wrappable { html { position: relative; min-height: 100%; + // hack to prevent horizontal overflow problem on showHTML view + overflow-x: hidden; } body.full-screen-body-background { @@ -171,6 +173,18 @@ ul { .responsive-container { position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden; } .responsive-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } +.positive-5 { + margin-top: 5px; +} + +.positive-10 { + padding-top: 10px; +} + +.positive-15 { + margin-top: 15px; +} + .negative-45 { margin-top: -45px; margin-bottom: -45px; @@ -180,10 +194,18 @@ ul { margin-top: -10px; } +.negative-28 { + margin-top: -28px; +} + .negative-35 { margin-top: -35px; } +.negative-30 { + margin-top: -30px; +} + .negative-5 { margin-top: -5px; } @@ -192,6 +214,10 @@ ul { margin-top: -15px; } +.negative-20 { + margin-top: -20px; +} + .landing-p { font-size: 18px !important; } @@ -383,6 +409,26 @@ ul { font-size: 30px; } +.big-text-field { + font-size: 30px; + height: 57px; +} + +.btn-responsive { + @media (max-width: 768px) { + font-size: 15px; + height: 26.5px; + padding-top: 1px; + } +} + +.field-responsive { + @media (max-width: 768px) { + font-size: 15px; + height: 26.5px; + } +} + .table { margin-left: -16px; } @@ -719,7 +765,6 @@ iframe.iphone { -ms-transition: background .2s ease-in-out, border .2s ease-in-out; -o-transition: background .2s ease-in-out, border .2s ease-in-out; transition: background .2s ease-in-out, border .2s ease-in-out; - } .hamburger { @@ -744,6 +789,56 @@ iframe.iphone { margin-bottom: -4px; } +.story-list { + padding-bottom: 30px; + margin-bottom: 30px; +} + +.big-ion-up-arrow { + font-size: 45px; + margin-top: -10px; + margin-bottom: -15px; + text-align: center; +} + +.story-up-votes { + padding-top: 0px; + margin-left: -5px; + text-align: center; +} + +.control-label .control-label-story-submission { + telt-align: left; +} + +.img-story-post { + max-width: 110px; + max-height: 110px; +} + +.spacer { + padding: 15px 0 15px 0; +} + +.img-news { + width: 50px; + height: 50px; +} + +//.media ~ .media .media-body-wrapper:nth-child(odd) { +// background-color: #e5e5e5; +//} + +.media-news { + width: 100%; +} + +.comment-to-comment-formgroup { + width: 50%; + height: 35px; +} + + //uncomment this to see the dimensions of all elements outlined in red //* { // border-color: red; diff --git a/public/js/application.js b/public/js/application.js index f792928b2b..eeebfa316b 100644 --- a/public/js/application.js +++ b/public/js/application.js @@ -15,5 +15,5 @@ //= require lib/jquery-2.1.1.min //= require lib/bootstrap.min -//= require lib/together/togetherjs +//= require lib/moment/moment //= require main diff --git a/public/js/lib/together/togetherjs.js b/public/js/lib/together/togetherjs.js deleted file mode 100644 index 5d8412c3b8..0000000000 --- a/public/js/lib/together/togetherjs.js +++ /dev/null @@ -1,786 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/*jshint scripturl:true */ -(function () { - - var defaultConfiguration = { - // Disables clicks for a certain element. - // (e.g., 'canvas' would not show clicks on canvas elements.) - // Setting this to true will disable clicks globally. - dontShowClicks: false, - // Experimental feature to echo clicks to certain elements across clients: - cloneClicks: false, - // Enable Mozilla or Google analytics on the page when TogetherJS is activated: - // FIXME: these don't seem to be working, and probably should be removed in favor - // of the hub analytics - enableAnalytics: false, - // The code to enable (this is defaulting to a Mozilla code): - analyticsCode: "UA-55446531-1", - // The base URL of the hub (gets filled in below): - hubBase: "https://fcctogether.herokuapp.com", - // A function that will return the name of the user: - getUserName: null, - // A function that will return the color of the user: - getUserColor: null, - // A function that will return the avatar of the user: - getUserAvatar: null, - // The siteName is used in the walkthrough (defaults to document.title): - siteName: null, - // Whether to use the minimized version of the code (overriding the built setting) - useMinimizedCode: undefined, - // Any events to bind to - on: {}, - // Hub events to bind to - hub_on: {}, - // Enables the alt-T alt-T TogetherJS shortcut; however, this setting - // must be enabled early as TogetherJSConfig_enableShortcut = true; - enableShortcut: false, - // The name of this tool as provided to users. The UI is updated to use this. - // Because of how it is used in text it should be a proper noun, e.g., - // "MySite's Collaboration Tool" - toolName: null, - // Used to auto-start TogetherJS with a {prefix: pageName, max: participants} - // Also with findRoom: "roomName" it will connect to the given room name - findRoom: null, - // If true, starts TogetherJS automatically (of course!) - autoStart: false, - // If true, then the "Join TogetherJS Session?" confirmation dialog - // won't come up - suppressJoinConfirmation: false, - // If true, then the "Invite a friend" window won't automatically come up - suppressInvite: false, - // A room in which to find people to invite to this session, - inviteFromRoom: null, - // This is used to keep sessions from crossing over on the same - // domain, if for some reason you want sessions that are limited - // to only a portion of the domain: - storagePrefix: "togetherjs", - // When true, we treat the entire URL, including the hash, as the identifier - // of the page; i.e., if you one person is on `http://example.com/#view1` - // and another person is at `http://example.com/#view2` then these two people - // are considered to be at completely different URLs - includeHashInUrl: false, - // When true, the WebRTC-based mic/chat will be disabled - disableWebRTC: false, - // When true, youTube videos will synchronize - youtube: true, - // Ignores the following console messages, disables all messages if set to true - ignoreMessages: ["cursor-update", "keydown", "scroll-update"], - // Ignores the following forms (will ignore all forms if set to true): - ignoreForms: [":password"] -}; - - var styleSheet = "/togetherjs/togetherjs.css"; - - var baseUrl = "https://togetherjs.com"; - if (baseUrl == "__" + "baseUrl__") { - // Reset the variable if it doesn't get substituted - baseUrl = ""; - } - // True if this file should use minimized sub-resources: - var min = "no" == "__" + "min__" ? false : "no" == "yes"; - - var baseUrlOverride = localStorage.getItem("togetherjs.baseUrlOverride"); - if (baseUrlOverride) { - try { - baseUrlOverride = JSON.parse(baseUrlOverride); - } catch (e) { - baseUrlOverride = null; - } - if ((! baseUrlOverride) || baseUrlOverride.expiresAt < Date.now()) { - // Ignore because it has expired - localStorage.removeItem("togetherjs.baseUrlOverride"); - } else { - baseUrl = baseUrlOverride.baseUrl; - var logger = console.warn || console.log; - logger.call(console, "Using TogetherJS baseUrlOverride:", baseUrl); - logger.call(console, "To undo run: localStorage.removeItem('togetherjs.baseUrlOverride')"); - } - } - - var configOverride = localStorage.getItem("togetherjs.configOverride"); - if (configOverride) { - try { - configOverride = JSON.parse(configOverride); - } catch (e) { - configOverride = null; - } - if ((! configOverride) || configOverride.expiresAt < Date.now()) { - localStorage.removeItem("togetherjs.configOverride"); - } else { - var shownAny = false; - for (var attr in configOverride) { - if (attr == "expiresAt" || ! configOverride.hasOwnProperty(attr)) { - continue; - } - if (! shownAny) { - console.warn("Using TogetherJS configOverride"); - console.warn("To undo run: localStorage.removeItem('togetherjs.configOverride')"); - } - window["TogetherJSConfig_" + attr] = configOverride[attr]; - console.log("Config override:", attr, "=", configOverride[attr]); - } - } - } - - var version = "unknown"; - // FIXME: we could/should use a version from the checkout, at least - // for production - var cacheBust = ""; - if ((! cacheBust) || cacheBust == "") { - cacheBust = Date.now() + ""; - } else { - version = cacheBust; - } - - // Make sure we have all of the console.* methods: - if (typeof console == "undefined") { - console = {}; - } - if (! console.log) { - console.log = function () {}; - } - ["debug", "info", "warn", "error"].forEach(function (method) { - if (! console[method]) { - console[method] = console.log; - } - }); - - if (! baseUrl) { - var scripts = document.getElementsByTagName("script"); - for (var i=0; i with togetherjs.js and togetherjs-min.js)"); - } - - function addStyle() { - var existing = document.getElementById("togetherjs-stylesheet"); - if (! existing) { - var link = document.createElement("link"); - link.id = "togetherjs-stylesheet"; - link.setAttribute("rel", "stylesheet"); - link.href = baseUrl + styleSheet + "?bust=" + cacheBust; - document.head.appendChild(link); - } - } - - function addScript(url) { - var script = document.createElement("script"); - script.src = baseUrl + url + "?bust=" + cacheBust; - document.head.appendChild(script); - } - - var TogetherJS = window.TogetherJS = function TogetherJS(event) { - if (TogetherJS.running) { - var session = TogetherJS.require("session"); - session.close(); - return; - } - TogetherJS.startup.button = null; - try { - if (event && typeof event == "object") { - if (event.target && typeof event) { - TogetherJS.startup.button = event.target; - } else if (event.nodeType == 1) { - TogetherJS.startup.button = event; - } else if (event[0] && event[0].nodeType == 1) { - // Probably a jQuery element - TogetherJS.startup.button = event[0]; - } - } - } catch (e) { - console.warn("Error determining starting button:", e); - } - if (window.TowTruckConfig) { - console.warn("TowTruckConfig is deprecated; please use TogetherJSConfig"); - if (window.TogetherJSConfig) { - console.warn("Ignoring TowTruckConfig in favor of TogetherJSConfig"); - } else { - window.TogetherJSConfig = TowTruckConfig; - } - } - if (window.TogetherJSConfig && (! window.TogetherJSConfig.loaded)) { - TogetherJS.config(window.TogetherJSConfig); - window.TogetherJSConfig.loaded = true; - } - - // This handles loading configuration from global variables. This - // includes TogetherJSConfig_on_*, which are attributes folded into - // the "on" configuration value. - var attr; - var attrName; - var globalOns = {}; - for (attr in window) { - if (attr.indexOf("TogetherJSConfig_on_") === 0) { - attrName = attr.substr(("TogetherJSConfig_on_").length); - globalOns[attrName] = window[attr]; - } else if (attr.indexOf("TogetherJSConfig_") === 0) { - attrName = attr.substr(("TogetherJSConfig_").length); - TogetherJS.config(attrName, window[attr]); - } else if (attr.indexOf("TowTruckConfig_on_") === 0) { - attrName = attr.substr(("TowTruckConfig_on_").length); - console.warn("TowTruckConfig_* is deprecated, please rename", attr, "to TogetherJSConfig_on_" + attrName); - globalOns[attrName] = window[attr]; - } else if (attr.indexOf("TowTruckConfig_") === 0) { - attrName = attr.substr(("TowTruckConfig_").length); - console.warn("TowTruckConfig_* is deprecated, please rename", attr, "to TogetherJSConfig_" + attrName); - TogetherJS.config(attrName, window[attr]); - } - - - } - // FIXME: copy existing config? - // FIXME: do this directly in TogetherJS.config() ? - // FIXME: close these configs? - var ons = TogetherJS.config.get("on"); - for (attr in globalOns) { - if (globalOns.hasOwnProperty(attr)) { - // FIXME: should we avoid overwriting? Maybe use arrays? - ons[attr] = globalOns[attr]; - } - } - TogetherJS.config("on", ons); - for (attr in ons) { - TogetherJS.on(attr, ons[attr]); - } - var hubOns = TogetherJS.config.get("hub_on"); - if (hubOns) { - for (attr in hubOns) { - if (hubOns.hasOwnProperty(attr)) { - TogetherJS.hub.on(attr, hubOns[attr]); - } - } - } - - if (! TogetherJS.startup.reason) { - // Then a call to TogetherJS() from a button must be started TogetherJS - TogetherJS.startup.reason = "started"; - } - - // FIXME: maybe I should just test for TogetherJS.require: - if (TogetherJS._loaded) { - var session = TogetherJS.require("session"); - addStyle(); - session.start(); - return; - } - // A sort of signal to session.js to tell it to actually - // start itself (i.e., put up a UI and try to activate) - TogetherJS.startup._launch = true; - - addStyle(); - var minSetting = TogetherJS.config.get("useMinimizedCode"); - TogetherJS.config.close("useMinimizedCode"); - if (minSetting !== undefined) { - min = !! minSetting; - } - var requireConfig = TogetherJS._extend(TogetherJS.requireConfig); - var deps = ["session", "jquery"]; - function callback(session, jquery) { - TogetherJS._loaded = true; - if (! min) { - TogetherJS.require = require.config({context: "togetherjs"}); - TogetherJS._requireObject = require; - } - } - if (! min) { - if (typeof require == "function") { - if (! require.config) { - console.warn("The global require (", require, ") is not requirejs; please use togetherjs-min.js"); - throw new Error("Conflict with window.require"); - } - TogetherJS.require = require.config(requireConfig); - } - } - if (typeof TogetherJS.require == "function") { - // This is an already-configured version of require - TogetherJS.require(deps, callback); - } else { - requireConfig.deps = deps; - requireConfig.callback = callback; - if (! min) { - window.require = requireConfig; - } - } - if (min) { - addScript("/togetherjs/togetherjsPackage.js"); - } else { - addScript("/togetherjs/libs/require.js"); - } - }; - - TogetherJS.pageLoaded = Date.now(); - - TogetherJS._extend = function (base, extensions) { - if (! extensions) { - extensions = base; - base = {}; - } - for (var a in extensions) { - if (extensions.hasOwnProperty(a)) { - base[a] = extensions[a]; - } - } - return base; - }; - - TogetherJS._startupInit = { - // What element, if any, was used to start the session: - button: null, - // The startReason is the reason TogetherJS was started. One of: - // null: not started - // started: hit the start button (first page view) - // joined: joined the session (first page view) - reason: null, - // Also, the session may have started on "this" page, or maybe is continued - // from a past page. TogetherJS.continued indicates the difference (false the - // first time TogetherJS is started or joined, true on later page loads). - continued: false, - // This is set to tell the session what shareId to use, if the boot - // code knows (mostly because the URL indicates the id). - _joinShareId: null, - // This tells session to start up immediately (otherwise it would wait - // for session.start() to be run) - _launch: false - }; - TogetherJS.startup = TogetherJS._extend(TogetherJS._startupInit); - TogetherJS.running = false; - - TogetherJS.requireConfig = { - context: "togetherjs", - baseUrl: baseUrl + "/togetherjs", - urlArgs: "bust=" + cacheBust, - paths: { - jquery: "libs/jquery-1.8.3.min", - walkabout: "libs/walkabout/walkabout", - esprima: "libs/walkabout/lib/esprima", - falafel: "libs/walkabout/lib/falafel", - tinycolor: "libs/tinycolor", - whrandom: "libs/whrandom/random" - } - }; - - TogetherJS._mixinEvents = function (proto) { - proto.on = function on(name, callback) { - if (typeof callback != "function") { - console.warn("Bad callback for", this, ".once(", name, ", ", callback, ")"); - throw "Error: .once() called with non-callback"; - } - if (name.search(" ") != -1) { - var names = name.split(/ +/g); - names.forEach(function (n) { - this.on(n, callback); - }, this); - return; - } - if (this._knownEvents && this._knownEvents.indexOf(name) == -1) { - var thisString = "" + this; - if (thisString.length > 20) { - thisString = thisString.substr(0, 20) + "..."; - } - console.warn(thisString + ".on('" + name + "', ...): unknown event"); - if (console.trace) { - console.trace(); - } - } - if (! this._listeners) { - this._listeners = {}; - } - if (! this._listeners[name]) { - this._listeners[name] = []; - } - if (this._listeners[name].indexOf(callback) == -1) { - this._listeners[name].push(callback); - } - }; - proto.once = function once(name, callback) { - if (typeof callback != "function") { - console.warn("Bad callback for", this, ".once(", name, ", ", callback, ")"); - throw "Error: .once() called with non-callback"; - } - var attr = "onceCallback_" + name; - // FIXME: maybe I should add the event name to the .once attribute: - if (! callback[attr]) { - callback[attr] = function onceCallback() { - callback.apply(this, arguments); - this.off(name, onceCallback); - delete callback[attr]; - }; - } - this.on(name, callback[attr]); - }; - proto.off = proto.removeListener = function off(name, callback) { - if (this._listenerOffs) { - // Defer the .off() call until the .emit() is done. - this._listenerOffs.push([name, callback]); - return; - } - if (name.search(" ") != -1) { - var names = name.split(/ +/g); - names.forEach(function (n) { - this.off(n, callback); - }, this); - return; - } - if ((! this._listeners) || ! this._listeners[name]) { - return; - } - var l = this._listeners[name], _len = l.length; - for (var i=0; i<_len; i++) { - if (l[i] == callback) { - l.splice(i, 1); - break; - } - } - }; - proto.emit = function emit(name) { - var offs = this._listenerOffs = []; - if ((! this._listeners) || ! this._listeners[name]) { - return; - } - var args = Array.prototype.slice.call(arguments, 1); - var l = this._listeners[name]; - l.forEach(function (callback) { - - callback.apply(this, args); - }, this); - delete this._listenerOffs; - if (offs.length) { - offs.forEach(function (item) { - this.off(item[0], item[1]); - }, this); - } - - }; - return proto; - }; - - /* This finalizes the unloading of TogetherJS, including unloading modules */ - TogetherJS._teardown = function () { - var requireObject = TogetherJS._requireObject || window.require; - // FIXME: this doesn't clear the context for min-case - if (requireObject.s && requireObject.s.contexts) { - delete requireObject.s.contexts.togetherjs; - } - TogetherJS._loaded = false; - TogetherJS.startup = TogetherJS._extend(TogetherJS._startupInit); - TogetherJS.running = false; - }; - - TogetherJS._mixinEvents(TogetherJS); - TogetherJS._knownEvents = ["ready", "close"]; - TogetherJS.toString = function () { - return "TogetherJS"; - }; - - TogetherJS._configuration = {}; - TogetherJS._defaultConfiguration = defaultConfiguration; - TogetherJS._configTrackers = {}; - TogetherJS._configClosed = {}; - - /* TogetherJS.config(configurationObject) - or: TogetherJS.config(configName, value) - - Adds configuration to TogetherJS. You may also set the global variable TogetherJSConfig - and when TogetherJS is started that configuration will be loaded. - - Unknown configuration values will lead to console error messages. - */ - TogetherJS.config = function (name, maybeValue) { - var settings; - if (arguments.length == 1) { - if (typeof name != "object") { - throw new Error('TogetherJS.config(value) must have an object value (not: ' + name + ')'); - } - settings = name; - } else { - settings = {}; - settings[name] = maybeValue; - } - var i; - var tracker; - for (var attr in settings) { - if (settings.hasOwnProperty(attr)) { - if (TogetherJS._configClosed[attr] && TogetherJS.running) { - throw new Error("The configuration " + attr + " is finalized and cannot be changed"); - } - } - } - for (var attr in settings) { - if (! settings.hasOwnProperty(attr)) { - continue; - } - if (attr == "loaded" || attr == "callToStart") { - continue; - } - if (! TogetherJS._defaultConfiguration.hasOwnProperty(attr)) { - console.warn("Unknown configuration value passed to TogetherJS.config():", attr); - } - var previous = TogetherJS._configuration[attr]; - var value = settings[attr]; - TogetherJS._configuration[attr] = value; - var trackers = TogetherJS._configTrackers[name] || []; - var failed = false; - for (i=0; i