├── .idea ├── .name ├── vcs.xml ├── jsLibraryMappings.xml ├── modules.xml ├── Goose-Windmill.iml ├── libraries │ └── Goose_Windmill_node_modules.xml ├── misc.xml └── workspace.xml ├── Procfile ├── favicon.ico ├── public ├── bookmark.png ├── bookmarked.png ├── app │ ├── bookmarks │ │ ├── bookmarks.html │ │ └── bookmarks.js │ ├── topStories │ │ ├── topStories.html │ │ └── topStories.js │ ├── currentlyFollowing │ │ ├── currentlyFollowing.js │ │ └── currentlyFollowing.html │ ├── topStoriesWithKeyword │ │ ├── topStoriesWithKeyword.html │ │ └── topStoriesWithKeyword.js │ ├── tabs │ │ ├── tabs.js │ │ └── tabs.html │ ├── services │ │ ├── auth.js │ │ ├── bookmarks.js │ │ ├── followers.js │ │ └── links.js │ ├── personal │ │ ├── personal.js │ │ └── personal.html │ ├── auth │ │ ├── auth.js │ │ └── auth.html │ ├── partials │ │ └── story.html │ └── app.js ├── lib │ ├── human-time.js │ ├── angular-route.min.js │ ├── angular-route.min.js.map │ └── underscore-min.js ├── index.html ├── dist │ ├── production.min.js │ ├── production.min.css │ ├── production.css │ └── production.js └── styles │ └── style.css ├── readme_assets └── hfscreenshot.png ├── .gitignore ├── index.js ├── server ├── cache │ ├── cacheRoutes.js │ ├── cacheController.js │ └── cacheModel.js ├── users │ ├── userRoutes.js │ ├── userController.js │ └── userModel.js ├── bookmarks │ ├── bookmarksRoutes.js │ ├── bookmarksController.js │ └── bookmarksModel.js ├── server.js └── config │ └── middleware.js ├── FLOOBITS_README.md ├── .jshintrc ├── bower.json ├── README.md ├── specs └── client │ └── routingSpecs.js ├── package.json ├── karma.conf.js ├── _PRESS-RELEASE.md ├── Gruntfile.js ├── _CONTRIBUTING.md └── _STYLE-GUIDE.md /.idea/.name: -------------------------------------------------------------------------------- 1 | Goose-Windmill -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepond/Goose-Windmill/HEAD/favicon.ico -------------------------------------------------------------------------------- /public/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepond/Goose-Windmill/HEAD/public/bookmark.png -------------------------------------------------------------------------------- /public/bookmarked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepond/Goose-Windmill/HEAD/public/bookmarked.png -------------------------------------------------------------------------------- /readme_assets/hfscreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepond/Goose-Windmill/HEAD/readme_assets/hfscreenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | *.log 4 | 5 | build/ 6 | 7 | 8 | #Floobits 9 | .floo 10 | .flooignore 11 | 12 | .DS_Store -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var app = require('./server/server.js'); 2 | var port = process.env.PORT || 3000; 3 | 4 | app.listen(port); 5 | 6 | console.log("Server running on port: " + port + "/\nCTRL + C to shutdown"); -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /server/cache/cacheRoutes.js: -------------------------------------------------------------------------------- 1 | var cacheController = require('./cacheController'); 2 | 3 | module.exports = function (app, router) { 4 | 5 | router 6 | .get('/topStories', cacheController.topStories) 7 | .get('/topStoriesWithKeyword', cacheController.topStoriesWithKeyword); 8 | 9 | }; -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FLOOBITS_README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Floobits! 2 | 3 | It looks like you're in an empty workspace. If you're using the web editor, you can 4 | click on the menu in the upper left to upload or create files. 5 | 6 | If you're using a native editor, you might want to read our help docs at 7 | https://floobits.com/help/plugins/ 8 | -------------------------------------------------------------------------------- /server/users/userRoutes.js: -------------------------------------------------------------------------------- 1 | var userController = require('./userController.js'); 2 | 3 | module.exports = function (app, router) { 4 | //Router routing to the controller 5 | router 6 | .post('/signup', userController.signup) 7 | .post('/signin', userController.signin) 8 | .post('/updateFollowing', userController.updateFollowing) 9 | } -------------------------------------------------------------------------------- /public/app/bookmarks/bookmarks.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
more
7 |
-------------------------------------------------------------------------------- /server/bookmarks/bookmarksRoutes.js: -------------------------------------------------------------------------------- 1 | var bookmarksController = require('./bookmarksController.js'); 2 | 3 | module.exports = function (app, router) { 4 | //Router routing to the controller 5 | router 6 | .post('/addBookmark', bookmarksController.addBookmark) 7 | .post('/removeBookmark', bookmarksController.removeBookmark) 8 | .post('/getBookmarks', bookmarksController.getBookmarks) 9 | } -------------------------------------------------------------------------------- /.idea/Goose-Windmill.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/app/topStories/topStories.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
more
7 |
8 | 9 | -------------------------------------------------------------------------------- /public/app/currentlyFollowing/currentlyFollowing.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.currentlyFollowing', []) 2 | 3 | .controller('CurrentlyFollowingController', function ($scope, Followers) { 4 | $scope.currentlyFollowing = Followers.following; 5 | 6 | $scope.unfollow = function(user){ 7 | Followers.removeFollower(user); 8 | }; 9 | 10 | $scope.follow = function(user){ 11 | Followers.addFollower(user); 12 | $scope.newFollow = ""; 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /.idea/libraries/Goose_Windmill_node_modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/app/currentlyFollowing/currentlyFollowing.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": false, 4 | "eqeqeq": true, 5 | "es3": false, 6 | "forin": false, 7 | "freeze": false, 8 | "noempty": false, 9 | "nonew": true, 10 | "plusplus": false, 11 | "strict": false, 12 | "laxbreak": true, 13 | "multistr": true, 14 | "eqnull": true, 15 | "expr": true, 16 | "asi": true, 17 | "immed": false, 18 | "latedef": false, 19 | "newcap": false, 20 | "noarg": false, 21 | "trailing": false, 22 | "loopfunc": true, 23 | "smarttabs": true 24 | } 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Goose-Windmill", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/Goose-Windmill/Goose-Windmill", 5 | "authors": [ 6 | "Siddharth Sukumar " 7 | ], 8 | "description": "\"Hacking hacker news\"", 9 | "moduleType": [ 10 | "globals", 11 | "node" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "angular": "~1.3.15", 23 | "underscore": "~1.8.3", 24 | "angular-mocks": "~1.3.15", 25 | "angular-route": "~1.3.15" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/app/topStoriesWithKeyword/topStoriesWithKeyword.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
Filter
5 | 6 | 7 |
8 |
9 |
10 |
11 | 12 |
more
13 |
14 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var mongoose = require('mongoose'); 3 | 4 | var app = express(); 5 | 6 | // ------ Connection to mongoose database ----- 7 | var uristring = 8 | process.env.MONGOLAB_URI || 9 | process.env.MONGOHQ_URL || 10 | 'mongodb://localhost/hfPersonal'; 11 | 12 | mongoose.connect(uristring, {}, function (err, res) { 13 | if (err) { 14 | console.log ('ERROR connecting to: ' + uristring + '. ' + err); 15 | } else { 16 | console.log ('Succeeded connected to: ' + uristring); 17 | } 18 | }); 19 | 20 | require('./config/middleware.js')(app, express); 21 | 22 | // app.listen(3000, function(){ 23 | // console.log("Listening on 3000"); 24 | // }); 25 | 26 | module.exports = app; -------------------------------------------------------------------------------- /public/app/tabs/tabs.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.tabs', []) 2 | 3 | .controller('TabsController', function ($scope, $location, $window, Links, Followers) { 4 | // If a user refreshes when the location is '/personal', 5 | // it will stay on '/personal'. 6 | var hash = $window.location.hash.split('/')[1]; 7 | hash = !hash ? 'all' : hash; 8 | $scope.currentTab = hash; 9 | 10 | // What is angle? Don't worry. This just makes the 11 | // refresh button do a cool spin animation. We splurged. 12 | $scope.angle = 360; 13 | 14 | $scope.changeTab = function(newTab){ 15 | $scope.currentTab = newTab; 16 | $location.path(newTab); 17 | }; 18 | 19 | $scope.refresh = function(){ 20 | Links.getTopStories(); 21 | Links.getPersonalStories(Followers.following); 22 | Links.getBookmarks(); 23 | $scope.angle += 360; 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /server/cache/cacheController.js: -------------------------------------------------------------------------------- 1 | var Cache = require('./cacheModel.js'); 2 | 3 | module.exports = { 4 | topStories: function(request, response) { 5 | Cache.getTopStories(function(err,results){ 6 | if(!err){ 7 | response.status(200).json(results); 8 | }else{ 9 | response.status(500).send(err); 10 | } 11 | }); 12 | }, 13 | 14 | topStoriesWithKeyword: function(request, response){ 15 | console.log("THADDEUS IS A NAVY SEAL " + request.query.keyword) 16 | Cache.getTopStoriesWithKeyword(request.query.keyword, function(err,results){ 17 | if(!err){ 18 | response.status(200).json(results); 19 | }else{ 20 | response.status(500).send(err); 21 | } 22 | }); 23 | } 24 | }; 25 | 26 | // Initialize and refresh the top story data every two minutes 27 | Cache.updateTopStories(); 28 | setInterval(Cache.updateTopStories, 120000); -------------------------------------------------------------------------------- /public/app/services/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.authService', []) 2 | 3 | .factory('Auth', function ($http, $location, $window) { 4 | var signin = function (user) { 5 | return $http({ 6 | method: 'POST', 7 | url: '/api/users/signin', 8 | data: user 9 | }) 10 | .then(function (resp) { 11 | return resp.data; 12 | }); 13 | }; 14 | 15 | var signup = function (user) { 16 | return $http({ 17 | method: 'POST', 18 | url: '/api/users/signup', 19 | data: user 20 | }) 21 | .then(function (resp) { 22 | return resp.data; 23 | }); 24 | }; 25 | 26 | var isAuth = function () { 27 | return !!$window.localStorage.getItem('com.hack'); 28 | }; 29 | 30 | var signout = function () { 31 | $window.localStorage.removeItem('com.hack'); 32 | }; 33 | 34 | 35 | return { 36 | signin: signin, 37 | signup: signup, 38 | isAuth: isAuth, 39 | signout: signout 40 | }; 41 | }); -------------------------------------------------------------------------------- /public/app/tabs/tabs.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | All 4 |
5 |
6 |
7 |
8 | 9 | Personal 10 |
11 |
12 |
13 |
14 | 15 | Bookmarks 16 |
17 |
18 |
19 |
20 |
-------------------------------------------------------------------------------- /server/config/middleware.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | 3 | module.exports = function(app, express){ 4 | //Static file locations 5 | app.use(express.static(__dirname + '/../../public')); 6 | app.use(express.static(__dirname + '/../../bower_components')); 7 | 8 | //Server App middleware 9 | app.use(bodyParser.json()); 10 | app.use(bodyParser.urlencoded({extended: false})); 11 | 12 | //Establish routers and inject the router into the routes file 13 | var userRouter = express.Router(); 14 | require('../users/userRoutes.js')(app, userRouter); 15 | 16 | var cacheRouter = express.Router(); 17 | require('../cache/cacheRoutes.js')(app, cacheRouter); 18 | 19 | var bookmarksRouter = express.Router(); 20 | require('../bookmarks/bookmarksRoutes.js')(app, bookmarksRouter); 21 | 22 | //Establish routes 23 | app.use('/api/users', userRouter); 24 | app.use('/api/cache', cacheRouter); 25 | app.use('/api/bookmarks', bookmarksRouter); 26 | }; 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/app/personal/personal.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.personal', []) 2 | 3 | .controller('PersonalController', function ($scope, $window, Links, Followers, Auth, Bookmarks) { 4 | $scope.stories = Links.personalStories; 5 | $scope.users = Followers.following; 6 | $scope.perPage = 30; 7 | $scope.index = $scope.perPage; 8 | $scope.loggedIn = Auth.isAuth(); 9 | 10 | $scope.isBookmark = function(story) { 11 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 12 | return false; 13 | } else { 14 | return true; 15 | } 16 | }; 17 | 18 | $scope.addBookmark = function(story) { 19 | Bookmarks.addBookmark(story); 20 | }; 21 | 22 | $scope.removeBookmark = function(story) { 23 | Bookmarks.removeBookmark(story); 24 | }; 25 | 26 | var init = function(){ 27 | fetchUsers(); 28 | }; 29 | 30 | var fetchUsers = function(){ 31 | Links.getPersonalStories($scope.users); 32 | Links.getBookmarks(); 33 | }; 34 | 35 | init(); 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /public/app/bookmarks/bookmarks.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.bookmarks', []) 2 | 3 | .controller('BookmarksController', function ($scope, $window, Links, Followers, Bookmarks, Auth) { 4 | $scope.currentBookmarks = Bookmarks.bookmarks; 5 | $scope.loggedIn = Auth.isAuth(); 6 | $scope.stories = Links.bookmarkStories; 7 | $scope.perPage = 30; 8 | $scope.index = $scope.perPage; 9 | $scope.currentlyFollowing = Followers.following; 10 | 11 | $scope.addUser = function(username) { 12 | Followers.addFollower(username); 13 | }; 14 | 15 | $scope.isBookmark = function(story) { 16 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 17 | return false; 18 | } else { 19 | return true; 20 | } 21 | }; 22 | $scope.addBookmark = function(story) { 23 | Bookmarks.addBookmark(story); 24 | }; 25 | $scope.removeBookmark = function(story) { 26 | Bookmarks.removeBookmark(story); 27 | }; 28 | 29 | var init = function () { 30 | Links.getBookmarks(); 31 | }; 32 | init(); 33 | }); -------------------------------------------------------------------------------- /public/app/personal/personal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | {{story.author}} 12 | {{story.created_at | fromNow}} | 13 | parent | 14 | on: {{story.story_title}} 15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
more
23 | -------------------------------------------------------------------------------- /public/app/topStories/topStories.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.topStories', []) 2 | 3 | .controller('TopStoriesController', function ($scope, $window, Links, Followers, Bookmarks, Auth) { 4 | angular.extend($scope, Links); 5 | $scope.stories = Links.topStories; 6 | $scope.perPage = 30; 7 | $scope.index = $scope.perPage; 8 | $scope.loggedIn = Auth.isAuth(); 9 | $scope.currentlyFollowing = Followers.following; 10 | 11 | $scope.getData = function() { 12 | Links.getTopStories(); 13 | }; 14 | 15 | $scope.addUser = function(username) { 16 | Followers.addFollower(username); 17 | }; 18 | 19 | $scope.isBookmark = function(story) { 20 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 21 | return false; 22 | } else { 23 | return true; 24 | } 25 | }; 26 | 27 | $scope.addBookmark = function(story) { 28 | Bookmarks.addBookmark(story); 29 | }; 30 | 31 | $scope.removeBookmark = function(story) { 32 | Bookmarks.removeBookmark(story); 33 | }; 34 | 35 | $scope.getData(); 36 | Links.getBookmarks(); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /public/app/auth/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.auth', []) 2 | 3 | .controller('AuthController', ["$scope", "$window", "$location", "Auth", "Followers", "Bookmarks", 4 | function ($scope, $window, $location, Auth, Followers, Bookmarks) { 5 | 6 | $scope.user = {}; 7 | $scope.newUser = {}; 8 | $scope.loggedIn = Auth.isAuth(); 9 | 10 | $scope.signin = function () { 11 | Auth.signin($scope.user) 12 | .then(function (followers, bookmarks) { 13 | $window.localStorage.setItem('com.hack', $scope.user.username); 14 | $window.localStorage.setItem('hfUsers', followers); 15 | 16 | Followers.localToArr(); 17 | 18 | $scope.loggedIn = true; 19 | $scope.user = {}; 20 | }) 21 | .catch(function (error) { 22 | console.error(error); 23 | }); 24 | }; 25 | 26 | $scope.signup = function () { 27 | $scope.newUser.following = Followers.following.join(','); 28 | 29 | Auth.signup($scope.newUser) 30 | .then(function (data) { 31 | $window.localStorage.setItem('com.hack', $scope.newUser.username); 32 | 33 | $scope.loggedIn = true; 34 | $scope.newUser = {}; 35 | }) 36 | .catch(function (error) { 37 | console.error(error); 38 | }); 39 | }; 40 | 41 | $scope.logout = function () { 42 | Auth.signout(); 43 | $scope.loggedIn = false; 44 | } 45 | }]); 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Ready](https://badge.waffle.io/goose-windmill/goose-windmill.png?label=ready&title=Ready)](https://waffle.io/goose-windmill/goose-windmill) 2 | # Goose-Windmill 3 | 4 | > A fresh, responsive and fully featured UI for Hacker News 5 | 6 | ![hack feed screenshot](/readme_assets/hfscreenshot.png) 7 | 8 | See the UI in action on our [Heroku deployment](http://hackfeed.herokuapp.com/#/) 9 | 10 | ## Team 11 | 12 | - __Product Owner__: Kenny Tran 13 | - __Scrum Master__: Matthew Rourke 14 | - __Development Team Members__: Jennifer Bland, Siddharth Sukumar, Lindsay Pond, Cooper Buckingham, William Kilmer, Angela Wang, Vincent Nocera 15 | 16 | ## Table of Contents 17 | 18 | 1. [Usage](#Usage) 19 | 1. [Requirements](#requirements) 20 | 1. [Development](#development) 21 | 1. [Installing Dependencies](#installing-dependencies) 22 | 1. [Tasks](#tasks) 23 | 1. [Team](#team) 24 | 1. [Contributing](#contributing) 25 | 26 | ## Usage 27 | 28 | > Some usage instructions 29 | 30 | ## Requirements 31 | 32 | - Node 33 | - Angular 34 | - Express 35 | - MongoDB 36 | 37 | ## Development 38 | 39 | ### Installing Dependencies 40 | 41 | From within the root directory: 42 | 43 | ```sh 44 | sudo npm install -g bower 45 | npm install 46 | bower install 47 | ``` 48 | 49 | ### Roadmap 50 | 51 | View the project roadmap [here](LINK_TO_PROJECT_ISSUES) 52 | 53 | 54 | ## Contributing 55 | 56 | See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. 57 | -------------------------------------------------------------------------------- /public/app/partials/story.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 |
8 | 9 | {{story.title}} 10 | 11 | {{story.url.split('/')[2]}} 12 |
13 | 14 | 15 | 16 | 17 | {{story.points}} points | 18 | 19 | {{ story.author}} | 20 | {{story.created_at | fromNow}} | 21 | 22 | {{story.num_comments ? story.num_comments + ' comments' : 'discuss'}} 23 | 24 |
25 |
26 |
-------------------------------------------------------------------------------- /server/bookmarks/bookmarksController.js: -------------------------------------------------------------------------------- 1 | var Bookmark = require('./bookmarksModel.js'); 2 | 3 | module.exports = { 4 | addBookmark: function(request, response, next) { 5 | // console.log('in server!!!', request.body); 6 | var username = request.body.username; 7 | var bookmark = request.body.bookmark; 8 | 9 | Bookmark.prototype.addBookmark(bookmark, username, function(err, results){ 10 | if(!err){ 11 | console.log('Bookmark data added'); 12 | response.status(200).end(); 13 | } else { 14 | console.log('Bookmark data add ERROR'); 15 | response.status(400).send(err); 16 | } 17 | }); 18 | }, 19 | removeBookmark: function(request, response, next) { 20 | var username = request.body.username; 21 | var bookmark = request.body.bookmark; 22 | 23 | Bookmark.prototype.removeBookmark(bookmark, username, function(err, results){ 24 | if(!err){ 25 | console.log('Bookmark data removed'); 26 | response.status(200).end(); 27 | } else { 28 | console.log('Bookmark data remove ERROR'); 29 | response.status(400).send(err); 30 | } 31 | }); 32 | }, 33 | getBookmarks: function(request, response, next) { 34 | var username = request.body.username; 35 | Bookmark.prototype.getBookmarks(username, function(err,results){ 36 | if(!err){ 37 | response.status(200).json(results); 38 | } else { 39 | response.status(500).send(err); 40 | } 41 | }); 42 | } 43 | }; -------------------------------------------------------------------------------- /public/app/topStoriesWithKeyword/topStoriesWithKeyword.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('hack.topStoriesWithKeyword', []) 3 | 4 | .controller('TopStoriesWithKeywordController', function ($scope, $window, Links, Followers, Bookmarks, Auth) { 5 | angular.extend($scope, Links); 6 | $scope.stories = Links.topStoriesWithKeyword; 7 | $scope.perPage = 30; 8 | $scope.index = $scope.perPage; 9 | $scope.loggedIn = Auth.isAuth(); 10 | $scope.keyword; 11 | $scope.checked = 'start'; 12 | 13 | // now i want to add a scope variable that is equal to the value of an input box 14 | // this might need to be a global variable so that its value can be set in the topStories page and still exist here 15 | // an alternative would be to click a link that takes us to the keyword page with the keyword initially set to '' 16 | 17 | $scope.currentlyFollowing = Followers.following; 18 | 19 | $scope.getData = function() { 20 | Links.getTopStoriesWithKeyword($scope.keyword); 21 | // $scope.checked = $scope.keyword; 22 | }; 23 | 24 | $scope.addUser = function(username) { 25 | Followers.addFollower(username); 26 | }; 27 | 28 | $scope.isBookmark = function(story) { 29 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 30 | return false; 31 | } else { 32 | return true; 33 | } 34 | }; 35 | 36 | $scope.addBookmark = function(story) { 37 | Bookmarks.addBookmark(story); 38 | }; 39 | 40 | $scope.removeBookmark = function(story) { 41 | Bookmarks.removeBookmark(story); 42 | }; 43 | 44 | $scope.getData(''); // the argument here will eventually be set by the input box 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /specs/client/routingSpecs.js: -------------------------------------------------------------------------------- 1 | describe('Routing', function () { 2 | // var $route; 3 | // beforeEach(module('shortly')); 4 | 5 | // beforeEach(inject(function($injector){ 6 | // $route = $injector.get('$route'); 7 | // })); 8 | 9 | it('Should say that true is true', function () { 10 | expect(true).to.eql(true); 11 | }); 12 | 13 | // it('Should have /signup route, template, and controller', function () { 14 | // expect($route.routes['/signup']).to.be.ok(); 15 | // expect($route.routes['/signup'].controller).to.be('AuthController'); 16 | // expect($route.routes['/signup'].templateUrl).to.be('app/auth/signup.html'); 17 | // }); 18 | 19 | // it('Should have /signin route, template, and controller', function () { 20 | // expect($route.routes['/signin']).to.be.ok(); 21 | // expect($route.routes['/signin'].controller).to.be('AuthController'); 22 | // expect($route.routes['/signin'].templateUrl).to.be('app/auth/signin.html'); 23 | // }); 24 | 25 | // it('Should have /links route, template, and controller', function () { 26 | // expect($route.routes['/links']).to.be.ok(); 27 | // expect($route.routes['/links'].controller).to.be('LinksController'); 28 | // expect($route.routes['/links'].templateUrl).to.be('app/links/links.html'); 29 | // }); 30 | 31 | // it('Should have /shorten route, template, and controller', function () { 32 | // expect($route.routes['/shorten']).to.be.ok(); 33 | // expect($route.routes['/shorten'].controller).to.be('ShortenController'); 34 | // expect($route.routes['/shorten'].templateUrl).to.be('app/shorten/shorten.html'); 35 | // }); 36 | }); 37 | -------------------------------------------------------------------------------- /public/app/auth/auth.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

HN Feed

4 |

Sign up to save your followers

5 |
6 |
7 |
8 | 9 |
10 | 18 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 35 | 36 | 37 | 38 |
39 |
40 |
41 |
42 |

HN Feed

43 | 44 |
45 |
-------------------------------------------------------------------------------- /public/app/services/bookmarks.js: -------------------------------------------------------------------------------- 1 | // HOW OUR FOLLOWING SYSTEM WORKS: 2 | // We want users to be able to follow people before they even 3 | // log in, because who actually has time to decide on a story/password? 4 | 5 | // So, we do this by saving the users that they follow into localStorage. 6 | // On signup, we'll send the users string in localStorage to our server 7 | // which wil save them to a database. 8 | 9 | angular.module('hack.bookmarkService', []) 10 | 11 | .factory('Bookmarks', function($http, $window) { 12 | var bookmarks = []; 13 | var user = $window.localStorage.getItem('com.hack'); 14 | 15 | var addBookmark = function(story){ 16 | 17 | var article = { 18 | points: story.points, 19 | url: story.url, 20 | title: story.title, 21 | author: story.author, 22 | created_at: story.created_at, 23 | objectID: story.objectID, 24 | num_comments: story.num_comments 25 | }; 26 | var data = { 27 | username: user, 28 | bookmark: article 29 | }; 30 | 31 | $http({ 32 | method: 'POST', 33 | url: '/api/bookmarks/addBookmark', 34 | data: data 35 | }); 36 | 37 | if (bookmarks.indexOf(article.objectID) === -1) { 38 | bookmarks.push(article.objectID); 39 | } 40 | }; 41 | 42 | var removeBookmark = function(story){ 43 | var article = { 44 | objectID: story.objectID 45 | }; 46 | 47 | var data = { 48 | username: user, 49 | bookmark: article 50 | }; 51 | 52 | $http({ 53 | method: 'POST', 54 | url: '/api/bookmarks/removeBookmark', 55 | data: data 56 | }); 57 | 58 | var splicePoint = bookmarks.indexOf(article.objectID); 59 | bookmarks.splice(splicePoint, 1); 60 | }; 61 | 62 | return { 63 | bookmarks: bookmarks, 64 | addBookmark: addBookmark, 65 | removeBookmark: removeBookmark 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Goose-Windmill", 3 | "version": "1.0.0", 4 | "description": "Hacker News follower functionality", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Goose-Windmill/Goose-Windmill" 12 | }, 13 | "keywords": [ 14 | "hacker", 15 | "news", 16 | "feed" 17 | ], 18 | "author": "Goose Windmil", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Goose-Windmill/Goose-Windmill" 22 | }, 23 | "homepage": "https://github.com/Goose-Windmill/Goose-Windmill", 24 | "dependencies": { 25 | "bcrypt": "^0.8.2", 26 | "bcrypt-nodejs": "0.0.3", 27 | "bluebird": "^2.9.25", 28 | "body-parser": "^1.12.3", 29 | "expect": "^1.6.0", 30 | "express": "^4.12.3", 31 | "grunt-contrib-concat": "^0.5.1", 32 | "grunt-contrib-cssmin": "^0.12.3", 33 | "grunt-contrib-jshint": "^0.11.2", 34 | "grunt-contrib-uglify": "^0.9.1", 35 | "grunt-contrib-watch": "^0.6.1", 36 | "grunt-karma": "^0.10.1", 37 | "grunt-ng-annotate": "^0.10.0", 38 | "grunt-nodemon": "^0.4.0", 39 | "grunt-shell": "^1.1.2", 40 | "jasmine-core": "^2.3.3", 41 | "karma": "^0.12.31", 42 | "karma-chrome-launcher": "^0.1.11", 43 | "karma-jasmine": "^0.3.5", 44 | "karma-nyan-reporter": "0.0.60", 45 | "karma-unicorn-reporter": "^0.1.4", 46 | "mongodb": "^2.0.28", 47 | "mongoose": "^4.0.2", 48 | "q": "^1.3.0", 49 | "request": "^2.55.0", 50 | "util": "^0.10.3" 51 | }, 52 | "devDependencies": { 53 | "expect.js": "^0.3.1", 54 | "grunt-concurrent": "^1.0.0", 55 | "grunt-open": "^0.2.3", 56 | "grunt-run": "^0.3.0", 57 | "grunt-services": "^0.1.0", 58 | "grunt-shell-spawn": "^0.3.8", 59 | "karma-chrome-launcher": "^0.1.11", 60 | "require": "^2.4.18" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/app/app.js: -------------------------------------------------------------------------------- 1 | angular.module('hack', [ 2 | 'hack.topStories', 3 | 'hack.topStoriesWithKeyword', 4 | 'hack.personal', 5 | 'hack.bookmarks', 6 | 'hack.bookmarkService', 7 | 'hack.currentlyFollowing', 8 | 'hack.linkService', 9 | 'hack.authService', 10 | 'hack.followService', 11 | 'hack.tabs', 12 | 'hack.auth', 13 | 'ngRoute' 14 | ]) 15 | 16 | .config(function($routeProvider, $httpProvider) { 17 | $routeProvider 18 | .when('/', { 19 | templateUrl: 'app/topStories/topStories.html', 20 | controller: 'TopStoriesController' 21 | }) 22 | .when('/personal', { 23 | templateUrl: 'app/personal/personal.html', 24 | controller: 'PersonalController' 25 | }) 26 | .when('/bookmarks', { 27 | templateUrl: 'app/bookmarks/bookmarks.html', 28 | controller: 'BookmarksController' 29 | }) 30 | .when('/keyword', { 31 | templateUrl: 'app/topStoriesWithKeyword/topStoriesWithKeyword.html', 32 | controller: 'TopStoriesWithKeywordController' 33 | }) 34 | .otherwise({ 35 | redirectTo: '/' 36 | }); 37 | }) 38 | 39 | .filter('fromNow', function(){ 40 | return function(date){ 41 | var foo = 3; 42 | return humanized_time_span(new Date(date)); 43 | } 44 | }) 45 | 46 | .filter('htmlsafe', ['$sce', function ($sce) { 47 | return function (text) { 48 | return $sce.trustAsHtml(text); 49 | }; 50 | }]) 51 | 52 | .directive('rotate', function () { 53 | return { 54 | restrict: 'A', 55 | link: function (scope, element, attrs) { 56 | scope.$watch(attrs.degrees, function (rotateDegrees) { 57 | var r = 'rotate(' + rotateDegrees + 'deg)'; 58 | // console.log(r); 59 | element.css({ 60 | '-moz-transform': r, 61 | '-webkit-transform': r, 62 | '-o-transform': r, 63 | '-ms-transform': r, 64 | 'transform': r 65 | }); 66 | }); 67 | } 68 | } 69 | }); 70 | 71 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Jun 30 2014 19:35:20 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | // angular source 19 | 'public/lib/angular.js', 20 | 'bower_components/angular-route/angular-route.js', 21 | 'public/lib/angular-mocks.js', 22 | 23 | // our app code 24 | 'public/app/**/*.js', 25 | 26 | // our spec files 27 | 'node_modules/expect.js/index.js', 28 | 'specs/client/**/*.js' 29 | ], 30 | 31 | 32 | // list of files to exclude 33 | exclude: [ 34 | 'karma.conf.js' 35 | ], 36 | 37 | 38 | // preprocess matching files before serving them to the browser 39 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 40 | preprocessors: { 41 | 42 | }, 43 | 44 | 45 | // test results reporter to use 46 | // possible values: 'dots', 'progress' 47 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 48 | reporters: ['nyan','unicorn'], 49 | 50 | 51 | // web server port 52 | port: 9876, 53 | 54 | 55 | // enable / disable colors in the output (reporters and logs) 56 | colors: true, 57 | 58 | 59 | // level of logging 60 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 61 | logLevel: config.LOG_INFO, 62 | 63 | 64 | // enable / disable watching file and executing tests whenever any file changes 65 | autoWatch: false, 66 | 67 | 68 | // start these browsers 69 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 70 | browsers: ['Chrome'], 71 | 72 | // Continuous Integration mode 73 | // if true, Karma captures browsers, runs the tests and exits 74 | singleRun: true 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /server/users/userController.js: -------------------------------------------------------------------------------- 1 | var User = require('./userModel.js'); 2 | 3 | module.exports = { 4 | //Handles new user account generation 5 | signup: function(request, response, next) { 6 | 7 | var username = request.body.username; 8 | var password = request.body.password; 9 | var following = request.body.following; 10 | 11 | var params = { 12 | username: username, 13 | password: password, 14 | following: following 15 | }; 16 | 17 | //First, determine if the username is available 18 | User.findOne({username: username}, function(err, user) { 19 | //User already exists, try again! 20 | if(user) { 21 | //Figure out a way for the client to redirect to the signup page 22 | //and inform the user that this username is already in use. 23 | response.status(400).send('Figure this out Kenny'); 24 | } else { 25 | //If it is not in use, create the user in the database 26 | User.prototype.createUser(params, function(err){ 27 | if(!err){ 28 | response.status(200).send("Signed up"); 29 | } else { 30 | response.status(400).send("Bad data"); 31 | } 32 | }); 33 | } 34 | }); 35 | }, 36 | 37 | //Interact with the database to validate username/password combination 38 | signin: function(request, response, next) { 39 | var username = request.body.username; 40 | var password = request.body.password; 41 | 42 | User.prototype.signin(username, password, function(err, results){ 43 | if(!err){ 44 | console.log('Signed in'); 45 | response.status(200).send(results); 46 | } else { 47 | console.log('Sign In error'); 48 | response.status(400).send(err); 49 | } 50 | }) 51 | }, 52 | 53 | //Controller tells the model to update the database when the user adds or 54 | //removes users from their following list 55 | updateFollowing: function(request, response, next) { 56 | var username = request.body.username; 57 | var following = request.body.following; 58 | 59 | User.prototype.updateFollowing(username, following, function(err, results){ 60 | if(!err){ 61 | console.log('User following data updated'); 62 | response.status(200).end(); 63 | } else { 64 | console.log('User following data update ERROR'); 65 | response.status(400).send(err); 66 | } 67 | }); 68 | } 69 | }; -------------------------------------------------------------------------------- /server/bookmarks/bookmarksModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Promise = require('bluebird'); 3 | 4 | var BookmarksSchema = mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true 8 | }, 9 | url: { 10 | type: String, 11 | required: true 12 | }, 13 | author: { 14 | type: String, 15 | required: true 16 | }, 17 | created_at: { 18 | type: String, 19 | required: true 20 | }, 21 | objectID: { 22 | type: String, 23 | required: true 24 | }, 25 | usernames: { 26 | type: [String], 27 | required: true 28 | }, 29 | points: { 30 | type: String, 31 | required: true 32 | }, 33 | num_comments: { 34 | type: String, 35 | required: true 36 | } 37 | }); 38 | 39 | var Bookmark = mongoose.model('bookmarks', BookmarksSchema); 40 | 41 | 42 | Bookmark.prototype.addBookmark = function (bookmark, username, callback){ 43 | //try to find the most recently clicked article in db by its ID 44 | Bookmark.findOne({objectID: bookmark.objectID}, 'usernames', function (err, result) { 45 | var allUsernames; 46 | if (err) console.log(err); 47 | //if article is not in db 48 | if (!result) { 49 | allUsernames = []; 50 | } else { 51 | //if article in db, then allUsernames is the result.usernames array 52 | //console.log('THIS IS THE RESULT OF FINDONE', result.usernames); 53 | allUsernames = result.usernames; 54 | } 55 | //regardless, add username to array, and upsert the article 56 | if (allUsernames.indexOf(username) === -1) { 57 | allUsernames.push(username); 58 | } 59 | Bookmark.update({objectID: bookmark.objectID}, 60 | { 61 | points: bookmark.points, 62 | title: bookmark.title, 63 | url: bookmark.url, 64 | author: bookmark.author, 65 | created_at: bookmark.created_at, 66 | objectID: bookmark.objectID, 67 | num_comments: bookmark.num_comments, 68 | usernames: allUsernames}, {upsert: true}, function(err, result) { 69 | if (err) console.log(err); 70 | }); 71 | }) 72 | }; 73 | 74 | Bookmark.prototype.removeBookmark = function (bookmark, username, callback) { 75 | Bookmark.findOne({objectID: bookmark.objectID}, 'usernames', function (err, result) { 76 | if (err) console.log(err); 77 | var allUsernames = result.usernames; 78 | var splicePoint = allUsernames.indexOf(username); 79 | allUsernames.splice(splicePoint, 1); 80 | Bookmark.update({objectID: bookmark.objectID}, 81 | { 82 | usernames: allUsernames 83 | }, {upsert: true}, function(err, result) { 84 | if (err) console.log(err); 85 | }); 86 | }) 87 | }; 88 | 89 | Bookmark.prototype.getBookmarks = function (username, callback) { 90 | Bookmark.find({usernames: username}, function (err, result) { 91 | if (err) console.log(err); 92 | callback(err, result); 93 | }) 94 | } 95 | 96 | module.exports = Bookmark; 97 | 98 | -------------------------------------------------------------------------------- /server/users/userModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Promise = require('bluebird'); 3 | var bcrypt = require('bcrypt-nodejs'); 4 | 5 | //Simple database of user data. Password is stored as a hash/salt combination. 6 | //The following value is a comma separated value of posters that the user is following. 7 | var UserSchema = mongoose.Schema({ 8 | username: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | hashword: { 14 | type: String, 15 | required: true 16 | }, 17 | following: { 18 | type: String, 19 | required: true 20 | }, 21 | }); 22 | 23 | var User = mongoose.model('users', UserSchema); 24 | 25 | //Model generates new users on signup. This function is only called after the controller 26 | //determines that the username is not taken. 27 | //params: an object that contains a string of the username, plain text string of the password, 28 | // and a comma separated string of users that the user is following 29 | User.prototype.createUser = function (params, callback){ 30 | //User password is stored as a hash/salt combination 31 | bcrypt.hash(params.password, null, null, function(err, hash){ 32 | if(hash) { 33 | var newUser = new User({ 34 | username: params.username, 35 | hashword: hash, 36 | following: params.following, 37 | }); 38 | newUser.save(function(err,results){ 39 | //Relay user creation success/failure back to the controller 40 | callback(err, results); 41 | }); 42 | } else { 43 | //bcrypt.hash error reporting 44 | callback(err); 45 | } 46 | }); 47 | }; 48 | 49 | //Model handles finding the user and password comparison 50 | User.prototype.signin = function (username, password, callback){ 51 | //Find if the user exists 52 | User.findOne({'username':username},function(err,user){ 53 | //If the user exists, compare hashed passwords 54 | if(user){ 55 | bcrypt.compare(password, user.hashword, function(err, res) { 56 | if (res) { 57 | //If correct, return the list of hacker news posters that the user is following 58 | callback(null, user.following); 59 | } else { 60 | //If not correct, handle error 61 | callback('Incorrect password', null); 62 | } 63 | }); 64 | } else { 65 | //If the user doesn't exist, handle error 66 | callback('Username not found', null); 67 | } 68 | }); 69 | }; 70 | 71 | //Update database when the user adds or removes users from their following list 72 | //Method operates on the assumption that the .signin has validated the username. 73 | User.prototype.updateFollowing = function (username, following, callback){ 74 | //Find the user in the database 75 | //Could try to refactor to .findOneAndUpdate or .findOneAndModify 76 | User.findOne({username: username}, function(err, user) { 77 | //Modify and save the database 78 | user.following = following; 79 | user.save(); 80 | callback(err); 81 | }); 82 | }; 83 | 84 | module.exports = User; -------------------------------------------------------------------------------- /public/app/services/followers.js: -------------------------------------------------------------------------------- 1 | // HOW OUR FOLLOWING SYSTEM WORKS: 2 | // We want users to be able to follow people before they even 3 | // log in, because who actually has time to decide on a username/password? 4 | 5 | // So, we do this by saving the users that they follow into localStorage. 6 | // On signup, we'll send the users string in localStorage to our server 7 | // which wil save them to a database. 8 | 9 | angular.module('hack.followService', []) 10 | 11 | .factory('Followers', function($http, $window) { 12 | var following = []; 13 | 14 | var updateFollowing = function(){ 15 | var user = $window.localStorage.getItem('com.hack'); 16 | 17 | if(!!user){ 18 | var data = { 19 | username: user, 20 | following: localStorageUsers() 21 | }; 22 | 23 | $http({ 24 | method: 'POST', 25 | url: '/api/users/updateFollowing', 26 | data: data 27 | }); 28 | } 29 | }; 30 | 31 | var addFollower = function(username){ 32 | var localFollowing = localStorageUsers(); 33 | 34 | if (!localFollowing.includes(username) && following.indexOf(username) === -1) { 35 | localFollowing += ',' + username 36 | $window.localStorage.setItem('hfUsers', localFollowing); 37 | following.push(username); 38 | } 39 | 40 | // makes call to database to mirror our changes 41 | updateFollowing(); 42 | }; 43 | 44 | var removeFollower = function(username){ 45 | var localFollowing = localStorageUsers(); 46 | 47 | if (localFollowing.includes(username) && following.indexOf(username) > -1) { 48 | following.splice(following.indexOf(username), 1); 49 | 50 | localFollowing = localFollowing.split(','); 51 | localFollowing.splice(localFollowing.indexOf(username), 1).join(','); 52 | $window.localStorage.setItem('hfUsers', localFollowing); 53 | } 54 | 55 | // makes call to database to mirror our changes 56 | updateFollowing(); 57 | }; 58 | 59 | var localStorageUsers = function(){ 60 | return $window.localStorage.getItem('hfUsers'); 61 | } 62 | 63 | 64 | // this function takes the csv in localStorage and turns it into an array. 65 | // There are pointers pointing to the 'following' array. The 'following' array 66 | // is how our controllers listen for changes and dynamically update the DOM. 67 | // (because you can't listen to localStorage changes) 68 | var localToArr = function(){ 69 | if(!localStorageUsers()){ 70 | // If the person is a new visitor, set pg and sama as the default 71 | // people to follow. Kinda like Tom on MySpace. Except less creepy. 72 | $window.localStorage.setItem('hfUsers', 'pg,sama'); 73 | } 74 | 75 | var users = localStorageUsers().split(','); 76 | 77 | following.splice(0, following.length); 78 | following.push.apply(following, users); 79 | }; 80 | 81 | 82 | 83 | var init = function(){ 84 | localToArr(); 85 | }; 86 | 87 | init(); 88 | 89 | return { 90 | following: following, 91 | addFollower: addFollower, 92 | removeFollower: removeFollower, 93 | localToArr: localToArr 94 | } 95 | }) 96 | -------------------------------------------------------------------------------- /_PRESS-RELEASE.md: -------------------------------------------------------------------------------- 1 | # Hacker Feed # 2 | 3 | 18 | 19 | Browse Hacker News more efficiently by telling Hacker Feed what you want to follow. 20 | 21 | ## Summary ## 22 | The Hacker News interface is outdated and does not have any options for personalize filtering. Hacker Feed exists to make Hacker News users time browsing more efficient by personalizing their feed. 23 | 24 | ## Problem ## 25 | Hacker News users do not have any options for filtering their content. 26 | 27 | ## Solution ## 28 | Hacker Feed allows its users to personalize their Hacker News reading experience by allowing them to follow users and topics that they find interesting. 29 | 30 | ## Quote from You ## 31 | "Hack your Hacker News experience." - Sid 32 | 33 | ## How to Get Started ## 34 | Go to the website, sign up for an account, and start following your favorite HN users and topics. 35 | 36 | ## Customer Quote ## 37 | "I cut my time browsing Hacker News by 50% while also reading only the interesting content." - Paul Graham 38 | 39 | ## Closing and Call to Action ## 40 | Visit LINK, sign up for an account, follow your favorite users and topics, and watch Hacker Feed do all your work for you! 41 | -------------------------------------------------------------------------------- /server/cache/cacheModel.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | //In server memory of Hacker News current top stories 4 | var topStories = []; 5 | var topStoriesWithKeyword = []; 6 | 7 | //Set headers 8 | var headers = { 9 | 'User-Agent': 'Hacker Feed', 10 | 'Content-Type': 'application/json' 11 | }; 12 | 13 | module.exports = { 14 | //Access function for model data 15 | getTopStories: function(callback) { 16 | if (topStories.length) { 17 | callback(null, topStories); 18 | } else { 19 | callback(new Error('Top Stories not cached!')); 20 | } 21 | }, 22 | 23 | getTopStoriesWithKeyword: function(keyword, callback) { 24 | topStoriesWithKeyword = []; 25 | // for (var i = 0; i < topStories.length; i++){ 26 | // if (topStories[i].title.search(keyword) !== -1){ 27 | // topStoriesWithKeyword.push(topStories[i]); 28 | // } 29 | // } 30 | var options = { 31 | url: 'http://hn.algolia.com/api/v1/search?query=' + keyword, 32 | method: 'GET', 33 | // params: {query: keyword} 34 | } 35 | 36 | request(options, function(error, response){ 37 | if (error) { 38 | return; 39 | } 40 | var data = JSON.parse(response.body); 41 | console.log("data length" + data.hits.length); 42 | // console.log("") 43 | for (var i = 0; i < data.hits.length; i ++){ 44 | topStoriesWithKeyword.push(data.hits[i]); 45 | // console.log(data.hits[i]); 46 | } 47 | console.log("ketword sotries length" + topStoriesWithKeyword.length); 48 | if (topStoriesWithKeyword.length) { 49 | callback(null, topStoriesWithKeyword); 50 | } else { 51 | callback(new Error('Top Stories with keyword not found!')); 52 | }; 53 | }) 54 | // console.log("what" + topStoriesWithKeyword.length); 55 | 56 | }, 57 | // The top news stories data is retrieved from the Algolia API, however it does not include 58 | // Hacker News' ranking algorithm. The data retrieved from Algolia is sorted according to the 59 | // ranking on the firebase API 60 | 61 | updateTopStories: function() { 62 | var storyOrderUrl = 'https://hacker-news.firebaseio.com/v0/topstories.json'; 63 | 64 | // Configure API request 65 | var options = { 66 | url: storyOrderUrl, 67 | method: 'GET', 68 | headers: headers 69 | }; 70 | 71 | // Perform the firebase API request 72 | request(options, function(error, response, html){ 73 | if (error) { 74 | return; 75 | } 76 | var data = JSON.parse(response.body); 77 | var storyOrder = data; 78 | 79 | // Generate algolia search API URL 80 | var storyUrl = 'http://hn.algolia.com/api/v1/search?hitsPerPage=500&tagFilters=story,('; 81 | var storyUrlIds = []; 82 | for(var i = 0; i < storyOrder.length; i++) { 83 | storyUrlIds.push('story_' + storyOrder[i]); 84 | } 85 | storyUrl += storyUrlIds.join(',') + ')'; 86 | options.url = storyUrl; 87 | 88 | request(options, function(error, response, html){ 89 | // Reorder the retrieved stories to match the hacker news front page 90 | 91 | var data = JSON.parse(response.body); 92 | var index; 93 | var indexMap = data.hits.map(function(obj) { 94 | return obj.objectID; 95 | }); 96 | 97 | // Clear out previous top stories 98 | topStories.length = 0; 99 | 100 | //storyOrder matches hacker news front page. Find data related to the story ID 101 | //in the incoming response data 102 | for(var i = 0; i < storyOrder.length; i++) { 103 | index = indexMap.indexOf(String(storyOrder[i])); 104 | var item = data.hits[index]; 105 | if(item){ 106 | topStories.push(data.hits[index]); 107 | } 108 | } 109 | console.log("Top Stories Updated"); 110 | console.log("# of stories " + topStories.length); 111 | }); 112 | }); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /public/app/services/links.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.linkService', []) 2 | 3 | .factory('Links', function($window, $http, $interval, Followers, Bookmarks) { 4 | var personalStories = []; 5 | var topStories = []; 6 | var bookmarkStories = []; 7 | 8 | 9 | var topStoriesWithKeyword = []; 10 | 11 | var getTopStories = function() { 12 | console.log('getTopStories'); 13 | var url = '/api/cache/topStories' 14 | 15 | return $http({ 16 | method: 'GET', 17 | url: url 18 | }) 19 | .then(function(resp) { 20 | 21 | // Very important to not point topStories to a new array. 22 | // Instead, clear out the array, then push all the new 23 | // datum in place. There are pointers pointing to this array. 24 | topStories.splice(0, topStories.length); 25 | topStories.push.apply(topStories, resp.data); 26 | }); 27 | }; 28 | 29 | var getTopStoriesWithKeyword = function(keyword) { 30 | console.log('getTopStoriesWithKeyword'); 31 | var url = '/api/cache/topStoriesWithKeyword' 32 | 33 | return $http({ 34 | method: 'GET', 35 | url: url, 36 | params: {keyword: keyword} 37 | }) 38 | .then(function(resp) { 39 | console.log(resp); 40 | 41 | // Very important to not point topStories to a new array. 42 | // Instead, clear out the array, then push all the new 43 | // datum in place. There are pointers pointing to this array. 44 | topStoriesWithKeyword.splice(0, topStoriesWithKeyword.length); 45 | topStoriesWithKeyword.push.apply(topStoriesWithKeyword, resp.data); 46 | console.log(topStoriesWithKeyword); 47 | }); 48 | } 49 | 50 | var getPersonalStories = function(usernames){ 51 | var query = 'http://hn.algolia.com/api/v1/search_by_date?hitsPerPage=500&tagFilters=(story,comment),('; 52 | var csv = arrToCSV(usernames); 53 | 54 | query += csv + ')'; 55 | 56 | return $http({ 57 | method: 'GET', 58 | url: query 59 | }) 60 | .then(function(resp) { 61 | angular.forEach(resp.data.hits, function(item){ 62 | // HN Comments don't have a title. So flag them as a comment. 63 | // This will come in handy when we decide how to render each item. 64 | if(item.title === null){ 65 | item.isComment = true; 66 | } 67 | }); 68 | 69 | // Very important to not point personalStories to a new array. 70 | // Instead, clear out the array, then push all the new 71 | // datum in place. There are pointers pointing to this array. 72 | personalStories.splice(0, personalStories.length); 73 | personalStories.push.apply(personalStories, resp.data.hits); 74 | }); 75 | }; 76 | 77 | var getBookmarks = function(){ 78 | var user = $window.localStorage.getItem('com.hack'); 79 | 80 | var data = {username: user}; 81 | return $http({ 82 | method: 'POST', 83 | url: '/api/bookmarks/getBookmarks', 84 | data: data 85 | }) 86 | .then(function(resp) { 87 | bookmarkStories.splice(0, bookmarkStories.length); 88 | angular.forEach(resp.data, function (story) { 89 | bookmarkStories.push(story); 90 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 91 | Bookmarks.bookmarks.push(story.objectID); 92 | } 93 | }); 94 | }); 95 | }; 96 | 97 | var arrToCSV = function(arr){ 98 | var holder = []; 99 | 100 | for(var i = 0; i < arr.length; i++){ 101 | holder.push('author_' + arr[i]); 102 | } 103 | 104 | return holder.join(','); 105 | }; 106 | 107 | var init = function(){ 108 | getPersonalStories(Followers.following); 109 | 110 | $interval(function(){ 111 | getPersonalStories(Followers.following); 112 | getTopStories(); 113 | }, 300000); 114 | }; 115 | 116 | init(); 117 | 118 | return { 119 | getTopStories: getTopStories, 120 | getTopStoriesWithKeyword: getTopStoriesWithKeyword, 121 | getPersonalStories: getPersonalStories, 122 | personalStories: personalStories, 123 | topStories: topStories, 124 | topStoriesWithKeyword: topStoriesWithKeyword, 125 | getBookmarks: getBookmarks, 126 | bookmarkStories: bookmarkStories 127 | }; 128 | }); 129 | 130 | 131 | -------------------------------------------------------------------------------- /public/lib/human-time.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 by Will Tomlins 2 | // 3 | // Github profile: http://github.com/layam 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | 24 | function humanized_time_span(date, ref_date, date_formats, time_units) { 25 | //Date Formats must be be ordered smallest -> largest and must end in a format with ceiling of null 26 | date_formats = date_formats || { 27 | past: [ 28 | { ceiling: 60, text: "$seconds seconds ago" }, 29 | { ceiling: 3600, text: "$minutes minutes ago" }, 30 | { ceiling: 86400, text: "$hours hours ago" }, 31 | { ceiling: 2629744, text: "$days days ago" }, 32 | { ceiling: 31556926, text: "$months months ago" }, 33 | { ceiling: null, text: "$years years ago" } 34 | ], 35 | future: [ 36 | { ceiling: 60, text: "in $seconds seconds" }, 37 | { ceiling: 3600, text: "in $minutes minutes" }, 38 | { ceiling: 86400, text: "in $hours hours" }, 39 | { ceiling: 2629744, text: "in $days days" }, 40 | { ceiling: 31556926, text: "in $months months" }, 41 | { ceiling: null, text: "in $years years" } 42 | ] 43 | }; 44 | //Time units must be be ordered largest -> smallest 45 | time_units = time_units || [ 46 | [31556926, 'years'], 47 | [2629744, 'months'], 48 | [86400, 'days'], 49 | [3600, 'hours'], 50 | [60, 'minutes'], 51 | [1, 'seconds'] 52 | ]; 53 | 54 | date = new Date(date); 55 | ref_date = ref_date ? new Date(ref_date) : new Date(); 56 | var seconds_difference = (ref_date - date) / 1000; 57 | 58 | var tense = 'past'; 59 | if (seconds_difference < 0) { 60 | tense = 'future'; 61 | seconds_difference = 0-seconds_difference; 62 | } 63 | 64 | function get_format() { 65 | for (var i=0; i 2 | 3 | 4 | 5 | Hack Feed 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 |
Select
19 |
20 | 21 |
22 | 25 | 28 | 31 | 34 |
35 | 36 |
37 |
38 |
Following
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 | 49 |
50 |
51 |
52 | 53 |
54 |
HN Feed
55 |
56 | 57 | 58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 | 73 | 75 | 76 |
77 |
78 | 79 |
80 |
81 | 82 | 90 | 91 | 92 | 93 |
94 |
95 |

Sign in to save your followers and bookmarks

96 |
97 | 98 |
99 | 100 |
101 |
102 | 103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | concat: { 6 | dist: { 7 | files: { 8 | 'public/dist/production.js': 9 | ['public/app/services/**.js', 10 | 'public/app/auth/auth.js', 11 | 'public/app/currentlyFollowing/currentlyFollowing.js', 12 | 'public/app/personal/personal.js', 13 | 'public/app/bookmarks/bookmarks.js', 14 | 'public/app/tabs/tabs.js', 15 | 'public/app/topStories/topStories.js', 16 | 'public/app/topStoriesWithKeyword/topStoriesWithKeyword.js', 17 | 'public/app/app.js'], 18 | 'public/dist/production.css': ['public/styles/*.css'] 19 | }, 20 | } 21 | }, 22 | 23 | nodemon: { 24 | dev: { 25 | script: 'index.js' 26 | } 27 | }, 28 | 29 | karma: { 30 | options: { 31 | // point all tasks to karma config file 32 | configFile: './karma.conf.js' 33 | }, 34 | unit: { 35 | // run tests once instead of continuously 36 | singleRun: true 37 | }, 38 | continuous: { 39 | // keep karma running in the background 40 | background: true 41 | } 42 | }, 43 | 44 | uglify: { 45 | build: { 46 | src: 'public/dist/production.js', 47 | dest: 'public/dist/production.min.js' 48 | } 49 | }, 50 | 51 | cssmin: { 52 | css:{ 53 | src: 'public/dist/production.css', 54 | dest: 'public/dist/production.min.css' 55 | } 56 | }, 57 | 58 | jshint: { 59 | files: [ 60 | 'public/app/**/*.js', 'server/**/*.js', 61 | ], 62 | options: { 63 | force: 'false', 64 | jshintrc: '.jshintrc', 65 | ignores: [ 66 | 'public/lib/**/*.js', 67 | 'public/dist/**/*.js' 68 | ] 69 | } 70 | }, 71 | 72 | // this task needs to be run after concat (and before uglify) is called on any angular files, 73 | ngAnnotate: { 74 | options: { 75 | add:true, 76 | }, 77 | dist: { 78 | files: { 79 | 'public/dist/production.js': ['public/dist/production.js'], 80 | } 81 | }, 82 | }, 83 | 84 | watch: { 85 | scripts: { 86 | files: [ 87 | 'public/**/*.js', 88 | 'public/lib/**/*.js', 89 | ], 90 | tasks: [ 91 | 'jshint', 92 | 'concat', 93 | 'ngAnnotate', 94 | 'uglify', 95 | 'cssmin' 96 | ] 97 | }, 98 | css: { 99 | files: 'public/**/*.css', 100 | tasks: ['cssmin'] 101 | }, 102 | karma: { 103 | // run these tasks when these files change 104 | files: ['public/app/**/*.js', 'test/**/*.js'], 105 | tasks: ['karma:continuous:run'] // note the :run flag 106 | } 107 | }, 108 | 109 | shell: { 110 | pull: { 111 | command: 'git pull --rebase upstream master' 112 | }, 113 | runDB: { 114 | command: 'mongod', 115 | options: { 116 | async: true 117 | } 118 | }, 119 | newTab:{ 120 | command: 'osascript -e \'tell application \"Terminal\" to activate\' -e \'tell application \"System Events\" to tell process \"Terminal\" to keystroke \"t\" using command down\'' 121 | }, 122 | push: { 123 | command: function(branch) { 124 | return 'git push origin ' + branch; 125 | } 126 | }, 127 | }, 128 | 129 | open: { 130 | all: { 131 | path: 'http://localhost:3000/#/' 132 | } 133 | } 134 | 135 | 136 | }); 137 | 138 | grunt.loadNpmTasks('grunt-contrib-concat'); 139 | grunt.loadNpmTasks('grunt-nodemon'); 140 | grunt.loadNpmTasks('grunt-contrib-uglify'); 141 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 142 | grunt.loadNpmTasks('grunt-contrib-jshint'); 143 | grunt.loadNpmTasks('grunt-ng-annotate'); 144 | grunt.loadNpmTasks('grunt-shell'); 145 | grunt.loadNpmTasks('grunt-contrib-watch'); 146 | grunt.loadNpmTasks('grunt-shell-spawn'); 147 | grunt.loadNpmTasks('grunt-run'); 148 | grunt.loadNpmTasks('grunt-services'); 149 | grunt.loadNpmTasks('grunt-open'); 150 | grunt.loadNpmTasks('grunt-karma'); 151 | grunt.loadNpmTasks('karma-chrome-launcher'); 152 | 153 | 154 | //To run this function, call the task gitFunctions and pass in the name of the branch that 155 | // will be pushed up to github 156 | //For example, grunt gitFunctions:newFeatureBranch 157 | //if no branch name is passed in, nothing happens 158 | grunt.registerTask('gitFunctions', 'Pull and push from github', function(n) { 159 | if (n){ 160 | grunt.task.run('shell:pull'); 161 | grunt.task.run('shell:push:' + n); 162 | } 163 | }); 164 | 165 | // this is to start and stop the Mongo server 166 | grunt.registerTask('start', 'Start all required services', ['startMongo']); 167 | grunt.registerTask('stop', 'Stop all services', ['stopMongo']); 168 | 169 | grunt.registerTask('server', function (target) { 170 | var nodemon = grunt.util.spawn({ 171 | cmd: 'grunt', 172 | grunt: true, 173 | args: ['nodemon'] 174 | }); 175 | nodemon.stdout.pipe(process.stdout); 176 | nodemon.stderr.pipe(process.stderr); 177 | 178 | // here you can run other tasks e.g. 179 | grunt.task.run([ 'watch' ]); 180 | 181 | }); 182 | 183 | //call this task to start the mongo server and the node server 184 | grunt.registerTask('startApp', [ 185 | 'start', 186 | 'server', 187 | ]); 188 | 189 | //call this task to run the test suite 190 | grunt.registerTask('test', ['karma:continuous:start', 'watch:karma']); 191 | 192 | //call this task before something is pushed to github 193 | grunt.registerTask('build', [ 194 | 'jshint', 195 | 'concat', 196 | 'ngAnnotate', 197 | 'uglify', 198 | 'cssmin' 199 | ]); 200 | 201 | }; -------------------------------------------------------------------------------- /_CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## General Workflow 4 | 5 | 1. Fork the repo 6 | 1. Cut a namespaced feature branch from master 7 | - bug/... 8 | - feat/... 9 | - test/... 10 | - doc/... 11 | - refactor/... 12 | 1. Make commits to your feature branch. Prefix each commit like so: 13 | - (feat) Added a new feature 14 | - (fix) Fixed inconsistent tests [Fixes #0] 15 | - (refactor) ... 16 | - (cleanup) ... 17 | - (test) ... 18 | - (doc) ... 19 | 1. When you've finished with your fix or feature, Rebase upstream changes into your branch. submit a [pull request][] 20 | directly to master. Include a description of your changes. 21 | 1. Your pull request will be reviewed by another maintainer. The point of code 22 | reviews is to help keep the codebase clean and of high quality and, equally 23 | as important, to help you grow as a programmer. If your code reviewer 24 | requests you make a change you don't understand, ask them why. 25 | 1. Fix any issues raised by your code reviwer, and push your fixes as a single 26 | new commit. 27 | 1. Once the pull request has been reviewed, it will be merged by another member of the team. Do not merge your own commits. 28 | 29 | ## Detailed Workflow 30 | 31 | ### Fork the repo 32 | 33 | Use github’s interface to make a fork of the repo, then add that repo as an upstream remote: 34 | 35 | ``` 36 | git remote add upstream https://github.com/hackreactor-labs/.git 37 | ``` 38 | 39 | ### Cut a namespaced feature branch from master 40 | 41 | Your branch should follow this naming convention: 42 | - bug/... 43 | - feat/... 44 | - test/... 45 | - doc/... 46 | - refactor/... 47 | 48 | These commands will help you do this: 49 | 50 | ``` bash 51 | 52 | # Creates your branch and brings you there 53 | git checkout -b `your-branch-name` 54 | ``` 55 | 56 | ### Make commits to your feature branch. 57 | 58 | Prefix each commit like so 59 | - (feat) Added a new feature 60 | - (fix) Fixed inconsistent tests [Fixes #0] 61 | - (refactor) ... 62 | - (cleanup) ... 63 | - (test) ... 64 | - (doc) ... 65 | 66 | Make changes and commits on your branch, and make sure that you 67 | only make changes that are relevant to this branch. If you find 68 | yourself making unrelated changes, make a new branch for those 69 | changes. 70 | 71 | #### Commit Message Guidelines 72 | 73 | - Commit messages should be written in the present tense; e.g. "Fix continuous 74 | integration script". 75 | - The first line of your commit message should be a brief summary of what the 76 | commit changes. Aim for about 70 characters max. Remember: This is a summary, 77 | not a detailed description of everything that changed. 78 | - If you want to explain the commit in more depth, following the first line should 79 | be a blank line and then a more detailed description of the commit. This can be 80 | as detailed as you want, so dig into details here and keep the first line short. 81 | 82 | ### Rebase upstream changes into your branch 83 | 84 | Once you are done making changes, you can begin the process of getting 85 | your code merged into the main repo. Step 1 is to rebase upstream 86 | changes to the master branch into yours by running this command 87 | from your branch: 88 | 89 | ```bash 90 | git pull --rebase upstream master 91 | ``` 92 | 93 | This will start the rebase process. You must commit all of your changes 94 | before doing this. If there are no conflicts, this should just roll all 95 | of your changes back on top of the changes from upstream, leading to a 96 | nice, clean, linear commit history. 97 | 98 | If there are conflicting changes, git will start yelling at you part way 99 | through the rebasing process. Git will pause rebasing to allow you to sort 100 | out the conflicts. You do this the same way you solve merge conflicts, 101 | by checking all of the files git says have been changed in both histories 102 | and picking the versions you want. Be aware that these changes will show 103 | up in your pull request, so try and incorporate upstream changes as much 104 | as possible. 105 | 106 | You pick a file by `git add`ing it - you do not make commits during a 107 | rebase. 108 | 109 | Once you are done fixing conflicts for a specific commit, run: 110 | 111 | ```bash 112 | git rebase --continue 113 | ``` 114 | 115 | This will continue the rebasing process. Once you are done fixing all 116 | conflicts you should run the existing tests to make sure you didn’t break 117 | anything, then run your new tests (there are new tests, right?) and 118 | make sure they work also. 119 | 120 | If rebasing broke anything, fix it, then repeat the above process until 121 | you get here again and nothing is broken and all the tests pass. 122 | 123 | ### Make a pull request 124 | 125 | Make a clear pull request from your fork and branch to the upstream master 126 | branch, detailing exactly what changes you made and what feature this 127 | should add. The clearer your pull request is the faster you can get 128 | your changes incorporated into this repo. 129 | 130 | At least one other person MUST give your changes a code review, and once 131 | they are satisfied they will merge your changes into upstream. Alternatively, 132 | they may have some requested changes. You should make more commits to your 133 | branch to fix these, then follow this process again from rebasing onwards. 134 | 135 | Once you get back here, make a comment requesting further review and 136 | someone will look at your code again. If they like it, it will get merged, 137 | else, just repeat again. 138 | 139 | Thanks for contributing! 140 | 141 | ### Guidelines 142 | 143 | 1. Uphold the current code standard: 144 | - Keep your code [DRY][]. 145 | - Apply the [boy scout rule][]. 146 | - Follow [STYLE-GUIDE.md](STYLE-GUIDE.md) 147 | 1. Run the [tests][] before submitting a pull request. 148 | 1. Tests are very, very important. Submit tests if your pull request contains 149 | new, testable behavior. 150 | 1. Your pull request is comprised of a single ([squashed][]) commit. 151 | 152 | ## Checklist: 153 | 154 | This is just to help you organize your process 155 | 156 | - [ ] Did I cut my work branch off of master (don't cut new branches from existing feature brances)? 157 | - [ ] Did I follow the correct naming convention for my branch? 158 | - [ ] Is my branch focused on a single main change? 159 | - [ ] Do all of my changes directly relate to this change? 160 | - [ ] Did I rebase the upstream master branch after I finished all my 161 | work? 162 | - [ ] Did I write a clear pull request message detailing what changes I made? 163 | - [ ] Did I get a code review? 164 | - [ ] Did I make any requested changes from that code review? 165 | 166 | If you follow all of these guidelines and make good changes, you should have 167 | no problem getting your changes merged in. 168 | 169 | 170 | 171 | [style guide]: https://github.com/hackreactor-labs/style-guide 172 | [n-queens]: https://github.com/hackreactor-labs/n-queens 173 | [Underbar]: https://github.com/hackreactor-labs/underbar 174 | [curriculum workflow diagram]: http://i.imgur.com/p0e4tQK.png 175 | [cons of merge]: https://f.cloud.github.com/assets/1577682/1458274/1391ac28-435e-11e3-88b6-69c85029c978.png 176 | [Bookstrap]: https://github.com/hackreactor/bookstrap 177 | [Taser]: https://github.com/hackreactor/bookstrap 178 | [tools workflow diagram]: http://i.imgur.com/kzlrDj7.png 179 | [Git Flow]: http://nvie.com/posts/a-successful-git-branching-model/ 180 | [GitHub Flow]: http://scottchacon.com/2011/08/31/github-flow.html 181 | [Squash]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 182 | -------------------------------------------------------------------------------- /public/dist/production.min.js: -------------------------------------------------------------------------------- 1 | angular.module("hack.authService",[]).factory("Auth",["$http","$location","$window",function(a,b,c){var d=function(b){return a({method:"POST",url:"/api/users/signin",data:b}).then(function(a){return a.data})},e=function(b){return a({method:"POST",url:"/api/users/signup",data:b}).then(function(a){return a.data})},f=function(){return!!c.localStorage.getItem("com.hack")},g=function(){c.localStorage.removeItem("com.hack")};return{signin:d,signup:e,isAuth:f,signout:g}}]),angular.module("hack.bookmarkService",[]).factory("Bookmarks",["$http","$window",function(a,b){var c=[],d=b.localStorage.getItem("com.hack"),e=function(b){var e={points:b.points,url:b.url,title:b.title,author:b.author,created_at:b.created_at,objectID:b.objectID,num_comments:b.num_comments},f={username:d,bookmark:e};a({method:"POST",url:"/api/bookmarks/addBookmark",data:f}),-1===c.indexOf(e.objectID)&&c.push(e.objectID)},f=function(b){var e={objectID:b.objectID},f={username:d,bookmark:e};a({method:"POST",url:"/api/bookmarks/removeBookmark",data:f});var g=c.indexOf(e.objectID);c.splice(g,1)};return{bookmarks:c,addBookmark:e,removeBookmark:f}}]),angular.module("hack.followService",[]).factory("Followers",["$http","$window",function(a,b){var c=[],d=function(){var c=b.localStorage.getItem("com.hack");if(c){var d={username:c,following:g()};a({method:"POST",url:"/api/users/updateFollowing",data:d})}},e=function(a){var e=g();e.includes(a)||-1!==c.indexOf(a)||(e+=","+a,b.localStorage.setItem("hfUsers",e),c.push(a)),d()},f=function(a){var e=g();e.includes(a)&&c.indexOf(a)>-1&&(c.splice(c.indexOf(a),1),e=e.split(","),e.splice(e.indexOf(a),1).join(","),b.localStorage.setItem("hfUsers",e)),d()},g=function(){return b.localStorage.getItem("hfUsers")},h=function(){g()||b.localStorage.setItem("hfUsers","pg,sama");var a=g().split(",");c.splice(0,c.length),c.push.apply(c,a)},i=function(){h()};return i(),{following:c,addFollower:e,removeFollower:f,localToArr:h}}]),angular.module("hack.linkService",[]).factory("Links",["$window","$http","$interval","Followers","Bookmarks",function(a,b,c,d,e){var f=[],g=[],h=[],i=[],j=function(){console.log("getTopStories");var a="/api/cache/topStories";return b({method:"GET",url:a}).then(function(a){g.splice(0,g.length),g.push.apply(g,a.data)})},k=function(a){console.log("getTopStoriesWithKeyword");var c="/api/cache/topStoriesWithKeyword";return b({method:"GET",url:c,params:{keyword:a}}).then(function(a){console.log(a),i.splice(0,i.length),i.push.apply(i,a.data),console.log(i)})},l=function(a){var c="http://hn.algolia.com/api/v1/search_by_date?hitsPerPage=500&tagFilters=(story,comment),(",d=n(a);return c+=d+")",b({method:"GET",url:c}).then(function(a){angular.forEach(a.data.hits,function(a){null===a.title&&(a.isComment=!0)}),f.splice(0,f.length),f.push.apply(f,a.data.hits)})},m=function(){var c=a.localStorage.getItem("com.hack"),d={username:c};return b({method:"POST",url:"/api/bookmarks/getBookmarks",data:d}).then(function(a){h.splice(0,h.length),angular.forEach(a.data,function(a){h.push(a),-1===e.bookmarks.indexOf(a.objectID)&&e.bookmarks.push(a.objectID)})})},n=function(a){for(var b=[],c=0;c git add . 248 | > git commit 249 | [save edits to the commit message file using the text editor that opens] 250 | 251 | # bad: 252 | > git commit -a 253 | [save edits to the commit message file using the text editor that opens] 254 | 255 | # bad: 256 | > git add . 257 | > git commit -m "updated algorithm" 258 | ``` 259 | 260 | 261 | ### Opening or closing too many blocks at once 262 | 263 | * The more blocks you open on a single line, the more your reader needs to remember about the context of what they are reading. Try to resolve your blocks early, and refactor. A good rule is to avoid closing more than two blocks on a single line--three in a pinch. 264 | 265 | ```javascript 266 | // avoid: 267 | _.ajax(url, {success: function(){ 268 | // ... 269 | }}); 270 | 271 | // prefer: 272 | _.ajax(url, { 273 | success: function(){ 274 | // ... 275 | } 276 | }); 277 | ``` 278 | 279 | 280 | ### Variable declaration 281 | 282 | * Use a new var statement for each line you declare a variable on. 283 | * Do not break variable declarations onto mutiple lines. 284 | * Use a new line for each variable declaration. 285 | * See http://benalman.com/news/2012/05/multiple-var-statements-javascript/ for more details 286 | 287 | ```javascript 288 | // good: 289 | var ape; 290 | var bat; 291 | 292 | // bad: 293 | var cat, 294 | dog 295 | 296 | // use sparingly: 297 | var eel, fly; 298 | ``` 299 | 300 | ### Capital letters in variable names 301 | 302 | * Some people choose to use capitalization of the first letter in their variable names to indicate that they contain a [class](http://en.wikipedia.org/wiki/Class_(computer_science\)). This capitalized variable might contain a function, a prototype, or some other construct that acts as a representative for the whole class. 303 | * Optionally, some people use a capital letter only on functions that are written to be run with the keyword `new`. 304 | * Do not use all-caps for any variables. Some people use this pattern to indicate an intended "constant" variable, but the language does not offer true constants, only mutable variables. 305 | 306 | 307 | ### Minutia 308 | 309 | * Don't rely on JavaScripts implicit global variables. If you are intending to write to the global scope, export things to `window.*` explicitly instead. 310 | 311 | ```javascript 312 | // good: 313 | var overwriteNumber = function(){ 314 | window.exported = Math.random(); 315 | }; 316 | 317 | // bad: 318 | var overwriteNumber = function(){ 319 | exported = Math.random(); 320 | }; 321 | ``` 322 | 323 | * For lists, put commas at the end of each newline, not at the beginning of each item in a list 324 | 325 | ```javascript 326 | // good: 327 | var animals = [ 328 | 'ape', 329 | 'bat', 330 | 'cat' 331 | ]; 332 | 333 | // bad: 334 | var animals = [ 335 | 'ape' 336 | , 'bat' 337 | , 'cat' 338 | ]; 339 | ``` 340 | 341 | * Avoid use of `switch` statements altogether. They are hard to outdent using the standard whitespace rules above, and are prone to error due to missing `break` statements. See [this article](http://ericleads.com/2012/12/switch-case-considered-harmful/) for more detail. 342 | 343 | * Prefer single quotes around JavaScript strings, rather than double quotes. Having a standard of any sort is preferable to a mix-and-match approach, and single quotes allow for easy embedding of HTML, which prefers double quotes around tag attributes. 344 | 345 | ```javascript 346 | // good: 347 | var dog = 'dog'; 348 | var cat = 'cat'; 349 | 350 | // acceptable: 351 | var dog = "dog"; 352 | var cat = "cat"; 353 | 354 | // bad: 355 | var dog = 'dog'; 356 | var cat = "cat"; 357 | ``` 358 | 359 | 360 | ### HTML 361 | 362 | * Do not use ids for html elements. Use a class instead. 363 | 364 | ```html 365 | 366 | 367 | 368 | 369 | 370 | ``` 371 | 372 | * Do not include a `type=text/javascript"` attribute on script tags 373 | 374 | ```html 375 | 376 | 377 | 378 | 379 | 380 | ``` 381 | -------------------------------------------------------------------------------- /public/dist/production.css: -------------------------------------------------------------------------------- 1 | /*CSS RESET*/ 2 | 3 | /* http://meyerweb.com/eric/tools/css/reset/*/ 4 | 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, figcaption, figure, 15 | footer, header, hgroup, menu, nav, section, summary, 16 | time, mark, audio, video { 17 | margin: 0; 18 | padding: 0; 19 | border: 0; 20 | outline: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | /* HTML5 display-role reset for older browsers */ 29 | article, aside, details, figcaption, figure, 30 | footer, header, hgroup, menu, nav, section { 31 | display: block; 32 | } 33 | body { 34 | line-height: 1; 35 | } 36 | ol, ul { 37 | list-style: none; 38 | } 39 | blockquote, q { 40 | quotes: none; 41 | } 42 | blockquote:before, blockquote:after, 43 | q:before, q:after { 44 | content: ''; 45 | content: none; 46 | } 47 | 48 | /* remember to define visible focus styles! */ 49 | :focus { 50 | outline: 0; 51 | } 52 | 53 | /* remember to highlight inserts somehow! */ 54 | ins { 55 | text-decoration: none; 56 | } 57 | del { 58 | text-decoration: line-through; 59 | } 60 | 61 | table { 62 | border-collapse: collapse; 63 | border-spacing: 0; 64 | } 65 | 66 | /*end*/ 67 | 68 | /*micro clearfix*/ 69 | .cf:before, 70 | .cf:after { 71 | content: " "; /* 1 */ 72 | display: table; /* 2 */ 73 | } 74 | 75 | .cf:after { 76 | clear: both; 77 | } 78 | /*end*/ 79 | 80 | select, input, textarea, button { 81 | -webkit-box-sizing: border-box; 82 | -moz-box-sizing: border-box; 83 | box-sizing: border-box; 84 | } 85 | 86 | input, button{ 87 | -webkit-appearance: none; 88 | -moz-appearance: none; 89 | appearance: none; 90 | 91 | } 92 | 93 | img, button, h1,h2{ 94 | -webkit-user-select: none; 95 | -moz-user-select: none; 96 | -ms-user-select: none; 97 | user-select: none; 98 | } 99 | 100 | body{ 101 | font-family: helvetica, arial, sans-serif; 102 | margin: 0; 103 | padding: 0; 104 | /*background: #F8F8F8;*/ 105 | height: 100%; 106 | width: 100%; 107 | vertical-align:top; 108 | min-width: 640px; 109 | min-height: 640px; 110 | overflow: hidden; 111 | } 112 | 113 | html { 114 | height: 100%; 115 | width: 100%; 116 | overflow-y: scroll; 117 | } 118 | 119 | a{ 120 | color: black; 121 | text-decoration: none; 122 | } 123 | 124 | .story-title:visited { 125 | color: #C8C8C8; 126 | } 127 | 128 | /*LEFT SIDE*/ 129 | 130 | html .left-container { 131 | width: 29%; 132 | min-width: 201px; 133 | max-width: 480px; 134 | height: 100%; 135 | display:inline-block; 136 | vertical-align:top; 137 | left: 0; 138 | top: 0; 139 | color: white; 140 | } 141 | 142 | .tabs{ 143 | height: 30%; 144 | background-color: white; 145 | } 146 | 147 | .tabs-content{ 148 | 149 | } 150 | 151 | button.tab { 152 | -webkit-transition: all 250 ease-in; 153 | -moz-transition: all 250 ease-in; 154 | -o-transition: all 250 ease-in; 155 | -ms-transition: all 250 ease-in; 156 | transition: all 250 ease-out; 157 | width: 100%; 158 | height: 25%; 159 | display:block; 160 | text-decoration: none; 161 | font-size: 2em; 162 | letter-spacing: -.1px; 163 | font-weight: bold; 164 | color: #303030; 165 | background-color: white; 166 | border: 0px solid white; 167 | position: relative; 168 | 169 | } 170 | 171 | html button.activeTab{ 172 | background-color: transparent; 173 | text-align: right; 174 | border-right: 10px solid #FFA500 175 | } 176 | 177 | button.activeTab:after{ 178 | } 179 | 180 | 181 | button.inactiveTab { 182 | text-align: right; 183 | background: #E8E8E8; 184 | opacity: 0.4; 185 | 186 | } 187 | .faded:hover{ 188 | opacity: 1; 189 | } 190 | 191 | .tabs-header-text{ 192 | color: black; 193 | } 194 | 195 | .leftSectionHolder{ 196 | width: 100%; 197 | margin: 0; 198 | color: white; 199 | display: block; 200 | position: relative; 201 | padding: 1px; 202 | overflow: hidden; 203 | } 204 | 205 | html .leftSectionHeaderHolder{ 206 | display: inline-block; 207 | width: auto; 208 | height: auto; 209 | } 210 | 211 | .leftSectionHeaderText{ 212 | font-weight: bold; 213 | text-align: center; 214 | font-size: 2em; 215 | letter-spacing: -.1em; 216 | -webkit-transform: rotate(-90deg)translate(-100%,0); 217 | -moz-transform: rotate(-90deg)translate(-100%,0); 218 | -ms-transform: rotate(-90deg)translate(-100%,0); 219 | -o-transform: rotate(-90deg)translate(-100%,0); 220 | transform: rotate(-90deg)translate(-100%,0); 221 | 222 | -webkit-transform-origin: left top; 223 | -moz-transform-origin: left top; 224 | -ms-transform-origin: left top; 225 | -o-transform-origin: left top; 226 | transform-origin: left top; 227 | } 228 | 229 | .leftSectionHolderContent{ 230 | display: inline-block; 231 | text-align: right; 232 | width: 90%; 233 | height: 100%; 234 | position: absolute; 235 | right: 0; 236 | top: 0; 237 | 238 | } 239 | 240 | 241 | .currently-following{ 242 | background-color: #404040; 243 | height: 40%; 244 | } 245 | 246 | .following-header{ 247 | 248 | } 249 | 250 | .following-header-text{ 251 | color: #FFA500; 252 | } 253 | 254 | .currently-following-right{ 255 | 256 | } 257 | 258 | .following-right-top{ 259 | display: inline-block; 260 | width: auto; 261 | height: auto; 262 | } 263 | 264 | html .following-right-top input { 265 | font-family: helvetica, arial, sans-serif; 266 | -webkit-box-sizing: border-box; 267 | -moz-box-sizing: border-box; 268 | box-sizing: border-box; 269 | display: inline-block; 270 | border-radius: 0; 271 | border: 0px solid white; 272 | font-size: 1.15em; 273 | margin: 1px; 274 | padding: 0px 0px 0px .1em; 275 | background-color: #FFA500; 276 | color: #404040; 277 | } 278 | 279 | html .following-right-top button { 280 | cursor: pointer; 281 | font-family: helvetica, arial, sans-serif; 282 | -webkit-box-sizing: border-box; 283 | -moz-box-sizing: border-box; 284 | box-sizing: border-box; 285 | display: inline-block; 286 | border-radius: 0; 287 | border: 0px solid white; 288 | font-size: 1.15em; 289 | font-weight: bold; 290 | margin: 1px; 291 | padding: .2px .5em .2px .5em; 292 | background-color: #FFA500; 293 | color: #404040; 294 | } 295 | 296 | html .following-right-bottom{ 297 | display: block; 298 | text-align: right; 299 | width:100%; 300 | height: 100%; 301 | padding-bottom: 1.3em; 302 | overflow-y: scroll; 303 | } 304 | 305 | .following-right-bottom button { 306 | border-radius: 0; 307 | margin: 1px; 308 | background-color: #FFA500; 309 | color: #404040; 310 | border: 0px solid white; 311 | cursor: pointer; 312 | float: right; 313 | font-size: 1em; 314 | } 315 | 316 | .colorOne{ 317 | color: #404040; 318 | } 319 | 320 | .colorTwo{ 321 | color: #FFA500; 322 | } 323 | 324 | html .followed-user{ 325 | /*border: 2px solid white;*/ 326 | 327 | 328 | } 329 | .followed-user:hover { 330 | background: lightgrey; 331 | } 332 | .followed-user:after{ 333 | content: 'x'; 334 | } 335 | 336 | .auth-header-text { 337 | color: #404040; 338 | } 339 | 340 | .auth-container { 341 | border-top: 2px solid #404040; 342 | border-right: 2px solid #FFA500; 343 | background-color: #FFA500; 344 | height: 30%; 345 | } 346 | 347 | .auth-content{ 348 | text-align: right; 349 | width: 80%; 350 | padding: 2px; 351 | } 352 | 353 | .auth-content button{ 354 | cursor: pointer; 355 | font-family: helvetica, arial, sans-serif; 356 | -webkit-box-sizing: border-box; 357 | -moz-box-sizing: border-box; 358 | box-sizing: border-box; 359 | display: inline-block; 360 | border-radius: 0; 361 | border: 0px solid white; 362 | font-size: 1.15em; 363 | font-weight: bold; 364 | margin: 1px; 365 | padding: .2px .5em .2px .5em; 366 | background-color: #FFA500; 367 | color: #404040; 368 | } 369 | 370 | .auth-content input{ 371 | font-family: helvetica, arial, sans-serif; 372 | -webkit-box-sizing: border-box; 373 | -moz-box-sizing: border-box; 374 | box-sizing: border-box; 375 | display: block; 376 | border-radius: 0; 377 | border: 0px solid white; 378 | font-size: 1.15em; 379 | margin: 2px; 380 | padding: 0px 0px 0px .1em; 381 | background-color: #404040; 382 | background-color: white; 383 | color: #FFA500; 384 | color: gray; 385 | width: 100%; 386 | 387 | } 388 | 389 | .loggedInDiv{ 390 | display: inline-block; 391 | width: 100%; 392 | height: 100%; 393 | } 394 | 395 | .loggedOutDiv{ 396 | display: inline-block; 397 | margin-top: 1em; 398 | height: 100%; 399 | margin-right: 5%; 400 | width: 95%; 401 | } 402 | 403 | 404 | /*.hero-header { 405 | font-size: 50px; 406 | top: 270px; 407 | position: fixed; 408 | transform: rotate(-90deg); 409 | transform-origin: left top 0; 410 | left: 95%; 411 | margin: 0; 412 | }*/ 413 | 414 | .feed { 415 | display: inline-block; 416 | width: 69%; 417 | height: 100%; 418 | } 419 | 420 | .inside-feed{ 421 | display: inline-block; 422 | position: relative; 423 | width: 100%; 424 | height: 100%; 425 | overflow-y: scroll; 426 | } 427 | 428 | .inside-feed button{ 429 | border: 0px solid white; 430 | 431 | } 432 | 433 | .story-whole{ 434 | display: inline-block; 435 | width: 100%; 436 | } 437 | 438 | .story-whole-left{ 439 | display: inline-block; 440 | width: 132px; 441 | height: 100%; 442 | text-align: right; 443 | } 444 | 445 | .story-whole-right{ 446 | display: inline-block; 447 | width: 75%; 448 | 449 | /*min-width: 768px;*/ 450 | } 451 | 452 | .story{ 453 | margin-bottom: .6em; 454 | position: relative; 455 | } 456 | 457 | .comment { 458 | margin: 2px; 459 | padding: 1.5em; 460 | background-color: #E8E8E8; 461 | 462 | } 463 | 464 | .story-title { 465 | font-size: 1em; 466 | font-weight: 600; 467 | font-family: 'open sans', helvetica, arial, sans-serif; 468 | } 469 | 470 | .bottom-row { 471 | /*color: grey;*/ 472 | opacity: 0.4; 473 | font-size: .9em; 474 | } 475 | 476 | .top-row { 477 | margin-bottom: 20px; 478 | opacity: 0.35; 479 | font-size: .95em; 480 | } 481 | 482 | .points-bar { 483 | display: inline-block; 484 | background: #D8D8D8; 485 | /*background: black;*/ 486 | height: 1em; 487 | box-sizing: border-box; 488 | max-width: 132px; 489 | margin-right: .2em; 490 | } 491 | 492 | /*.active { 493 | width: 0; 494 | height: 0; 495 | border-top: 10px solid transparent; 496 | border-bottom: 10px solid transparent; 497 | border-left: 10px solid #585858; 498 | 499 | right: -5px; 500 | position: absolute; 501 | top: 38px; 502 | }*/ 503 | 504 | .source-url { 505 | opacity: 0.4; 506 | font-size: .9em; 507 | } 508 | 509 | .filterBar{ 510 | display: inline-block; 511 | width: 100%; 512 | background-color: #404040; 513 | padding: 2px; 514 | } 515 | 516 | .filterBarTitle{ 517 | display: inline-block; 518 | font-family: helvetica, arial, sans-serif; 519 | font-size: 1.2em; 520 | font-weight: bold; 521 | color: #FFA500; 522 | } 523 | 524 | html .filterBar input { 525 | font-family: helvetica, arial, sans-serif; 526 | -webkit-box-sizing: border-box; 527 | -moz-box-sizing: border-box; 528 | box-sizing: border-box; 529 | display: inline-block; 530 | border-radius: 0; 531 | border: 0px solid white; 532 | font-size: 1.15em; 533 | margin: 2px; 534 | padding: .15em 1em .15em 1em; 535 | background-color: #FFA500; 536 | color: #404040; 537 | } 538 | 539 | .more-button { 540 | display: block; 541 | width: 80%; 542 | margin-top: 1em; 543 | padding-top: 1em; 544 | padding-bottom: 1em; 545 | margin-right: auto; 546 | margin-left: auto; 547 | text-align: center; 548 | /*background: #e8e8e8;*/ 549 | border-top: 2px solid black; 550 | border-left: 2px solid black; 551 | border-right: 2px solid black; 552 | font-size: 1.6em; 553 | font-weight: bold; 554 | letter-spacing: -3px; 555 | opacity: 0.55; 556 | border-top-right-radius: 1em; 557 | border-top-left-radius: 1em; 558 | } 559 | .more-button:hover { 560 | opacity: 1; 561 | text-decoration: none; 562 | } 563 | 564 | .new-follow { 565 | 566 | } 567 | 568 | #signin { 569 | 570 | } 571 | 572 | .auth-info { 573 | } 574 | 575 | .logged-header { 576 | color: #404040; 577 | } 578 | 579 | 580 | 581 | .story-page{ 582 | position: relative; 583 | } 584 | 585 | .story-holder{ 586 | position: relative; 587 | } 588 | 589 | .story-holder a:hover { 590 | text-decoration: underline; 591 | } 592 | 593 | .refresh { 594 | position: absolute; 595 | display: inline-block; 596 | z-index: 999; 597 | left: 0; 598 | 599 | width: 40px; 600 | height: 40px; 601 | background: url(http://findicons.com/files/icons/2579/iphone_icons/40/reload.png); 602 | background-repeat: no-repeat; 603 | background-size: contain; 604 | opacity: 0.15; 605 | 606 | -webkit-transition: all 600ms ease-out; 607 | -moz-transition: all 600ms ease-out; 608 | -o-transition: all 600ms ease-out; 609 | -ms-transition: all 600ms ease-out; 610 | transition: all 600ms ease-out; 611 | } 612 | 613 | .refresh:hover { 614 | opacity: 0.7; 615 | } 616 | 617 | .clickDown{ 618 | -webkit-transform: scale(.94,.94); 619 | -moz-transform: scale(.94,.94); 620 | -ms-transform: scale(.94,.94); 621 | -o-transform: scale(.94,.94); 622 | transform: scale(.94,.94); 623 | } 624 | 625 | @media screen and (max-width: 1700px) { 626 | /*space for another column on the right here*/ 627 | } 628 | 629 | 630 | @media screen and (max-width: 1520px) { 631 | .points-bar{ 632 | 633 | } 634 | } 635 | 636 | @media screen and (max-width: 900px) { 637 | .points-bar{ 638 | display: none; 639 | } 640 | .story-whole-left{ 641 | display: none; 642 | } 643 | .story-whole-right{ 644 | width: 100%; 645 | margin: 2px; 646 | } 647 | } 648 | 649 | @media screen and (max-width: 768px) { 650 | .story-whole-right{ 651 | 652 | } 653 | 654 | html .leftSectionHeaderHolder{ 655 | display: block; 656 | text-align: right; 657 | } 658 | 659 | .leftSectionHeaderText{ 660 | font-weight: bold; 661 | text-align: right; 662 | font-size: 2em; 663 | letter-spacing: -.1em; 664 | margin-right: 2px; 665 | -webkit-transform: rotate(0)translate(0,0); 666 | -moz-transform: rotate(0)translate(0,0); 667 | -ms-transform: rotate(0)translate(0,0); 668 | -o-transform: rotate(0)translate(0,0); 669 | transform: rotate(0)translate(0,0); 670 | 671 | -webkit-transform-origin: left top; 672 | -moz-transform-origin: left top; 673 | -ms-transform-origin: left top; 674 | -o-transform-origin: left top; 675 | transform-origin: left top; 676 | } 677 | 678 | .leftSectionHolderContent{ 679 | display: block; 680 | position: relative; 681 | width: 100%; 682 | } 683 | 684 | html .tabs .leftSectionHeaderHolder{ 685 | height: 10%; 686 | } 687 | 688 | button.tab{ 689 | height: 22.5%; 690 | } 691 | 692 | } 693 | 694 | @media screen and (max-width: 700px) { 695 | html .left-container{ 696 | width: 100%; 697 | display: inline-block; 698 | height: 30%; 699 | max-width: 100%; 700 | } 701 | html .feed{ 702 | width: 100%; 703 | display: inline-block; 704 | height: 70%; 705 | } 706 | 707 | .leftSectionHolder{ 708 | display: inline-block; 709 | vertical-align: top; 710 | border: 0px solid white; 711 | height: 100%; 712 | width: 33.3333333%; 713 | } 714 | 715 | } 716 | 717 | 718 | 719 | 720 | -------------------------------------------------------------------------------- /public/styles/style.css: -------------------------------------------------------------------------------- 1 | /*CSS RESET*/ 2 | 3 | /* http://meyerweb.com/eric/tools/css/reset/*/ 4 | 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, figcaption, figure, 15 | footer, header, hgroup, menu, nav, section, summary, 16 | time, mark, audio, video { 17 | margin: 0; 18 | padding: 0; 19 | border: 0; 20 | outline: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | /* HTML5 display-role reset for older browsers */ 29 | article, aside, details, figcaption, figure, 30 | footer, header, hgroup, menu, nav, section { 31 | display: block; 32 | } 33 | body { 34 | line-height: 1; 35 | } 36 | ol, ul { 37 | list-style: none; 38 | } 39 | blockquote, q { 40 | quotes: none; 41 | } 42 | blockquote:before, blockquote:after, 43 | q:before, q:after { 44 | content: ''; 45 | content: none; 46 | } 47 | 48 | /* remember to define visible focus styles! */ 49 | :focus { 50 | outline: 0; 51 | } 52 | 53 | /* remember to highlight inserts somehow! */ 54 | ins { 55 | text-decoration: none; 56 | } 57 | del { 58 | text-decoration: line-through; 59 | } 60 | 61 | table { 62 | border-collapse: collapse; 63 | border-spacing: 0; 64 | } 65 | 66 | /*end*/ 67 | 68 | /*micro clearfix*/ 69 | .cf:before, 70 | .cf:after { 71 | content: " "; /* 1 */ 72 | display: table; /* 2 */ 73 | } 74 | 75 | .cf:after { 76 | clear: both; 77 | } 78 | /*end*/ 79 | 80 | select, input, textarea, button { 81 | -webkit-box-sizing: border-box; 82 | -moz-box-sizing: border-box; 83 | box-sizing: border-box; 84 | } 85 | 86 | input, button{ 87 | -webkit-appearance: none; 88 | -moz-appearance: none; 89 | appearance: none; 90 | 91 | } 92 | 93 | img, button, h1,h2{ 94 | -webkit-user-select: none; 95 | -moz-user-select: none; 96 | -ms-user-select: none; 97 | user-select: none; 98 | } 99 | 100 | body{ 101 | font-family: helvetica, arial, sans-serif; 102 | margin: 0; 103 | padding: 0; 104 | /*background: #F8F8F8;*/ 105 | height: 100%; 106 | width: 100%; 107 | vertical-align:top; 108 | min-width: 640px; 109 | min-height: 640px; 110 | overflow: hidden; 111 | } 112 | 113 | html { 114 | height: 100%; 115 | width: 100%; 116 | overflow-y: scroll; 117 | } 118 | 119 | a{ 120 | color: black; 121 | text-decoration: none; 122 | } 123 | 124 | .story-title:visited { 125 | color: #C8C8C8; 126 | } 127 | 128 | /*LEFT SIDE*/ 129 | 130 | html .left-container { 131 | width: 29%; 132 | min-width: 201px; 133 | max-width: 480px; 134 | height: 100%; 135 | display:inline-block; 136 | vertical-align:top; 137 | left: 0; 138 | top: 0; 139 | color: white; 140 | } 141 | 142 | .tabs{ 143 | height: 30%; 144 | background-color: white; 145 | } 146 | 147 | .tabs-content{ 148 | 149 | } 150 | 151 | button.tab { 152 | -webkit-transition: all 250 ease-in; 153 | -moz-transition: all 250 ease-in; 154 | -o-transition: all 250 ease-in; 155 | -ms-transition: all 250 ease-in; 156 | transition: all 250 ease-out; 157 | width: 100%; 158 | height: 25%; 159 | display:block; 160 | text-decoration: none; 161 | font-size: 2em; 162 | letter-spacing: -.1px; 163 | font-weight: bold; 164 | color: #303030; 165 | background-color: white; 166 | border: 0px solid white; 167 | position: relative; 168 | 169 | } 170 | 171 | html button.activeTab{ 172 | background-color: transparent; 173 | text-align: right; 174 | border-right: 10px solid #FFA500 175 | } 176 | 177 | button.activeTab:after{ 178 | } 179 | 180 | 181 | button.inactiveTab { 182 | text-align: right; 183 | background: #E8E8E8; 184 | opacity: 0.4; 185 | 186 | } 187 | .faded:hover{ 188 | opacity: 1; 189 | } 190 | 191 | .tabs-header-text{ 192 | color: black; 193 | } 194 | 195 | .leftSectionHolder{ 196 | width: 100%; 197 | margin: 0; 198 | color: white; 199 | display: block; 200 | position: relative; 201 | padding: 1px; 202 | overflow: hidden; 203 | } 204 | 205 | html .leftSectionHeaderHolder{ 206 | display: inline-block; 207 | width: auto; 208 | height: auto; 209 | } 210 | 211 | .leftSectionHeaderText{ 212 | font-weight: bold; 213 | text-align: center; 214 | font-size: 2em; 215 | letter-spacing: -.1em; 216 | -webkit-transform: rotate(-90deg)translate(-100%,0); 217 | -moz-transform: rotate(-90deg)translate(-100%,0); 218 | -ms-transform: rotate(-90deg)translate(-100%,0); 219 | -o-transform: rotate(-90deg)translate(-100%,0); 220 | transform: rotate(-90deg)translate(-100%,0); 221 | 222 | -webkit-transform-origin: left top; 223 | -moz-transform-origin: left top; 224 | -ms-transform-origin: left top; 225 | -o-transform-origin: left top; 226 | transform-origin: left top; 227 | } 228 | 229 | .leftSectionHolderContent{ 230 | display: inline-block; 231 | text-align: right; 232 | width: 90%; 233 | height: 100%; 234 | position: absolute; 235 | right: 0; 236 | top: 0; 237 | 238 | } 239 | 240 | 241 | .currently-following{ 242 | background-color: #404040; 243 | height: 40%; 244 | } 245 | 246 | .following-header{ 247 | 248 | } 249 | 250 | .following-header-text{ 251 | color: #FFA500; 252 | } 253 | 254 | .currently-following-right{ 255 | 256 | } 257 | 258 | .following-right-top{ 259 | display: inline-block; 260 | width: auto; 261 | height: auto; 262 | } 263 | 264 | html .following-right-top input { 265 | font-family: helvetica, arial, sans-serif; 266 | -webkit-box-sizing: border-box; 267 | -moz-box-sizing: border-box; 268 | box-sizing: border-box; 269 | display: inline-block; 270 | border-radius: 0; 271 | border: 0px solid white; 272 | font-size: 1.15em; 273 | margin: 1px; 274 | padding: 0px 0px 0px .1em; 275 | background-color: #FFA500; 276 | color: #404040; 277 | } 278 | 279 | html .following-right-top button { 280 | cursor: pointer; 281 | font-family: helvetica, arial, sans-serif; 282 | -webkit-box-sizing: border-box; 283 | -moz-box-sizing: border-box; 284 | box-sizing: border-box; 285 | display: inline-block; 286 | border-radius: 0; 287 | border: 0px solid white; 288 | font-size: 1.15em; 289 | font-weight: bold; 290 | margin: 1px; 291 | padding: .2px .5em .2px .5em; 292 | background-color: #FFA500; 293 | color: #404040; 294 | } 295 | 296 | html .following-right-bottom{ 297 | display: block; 298 | text-align: right; 299 | width:100%; 300 | height: 100%; 301 | padding-bottom: 1.3em; 302 | overflow-y: scroll; 303 | } 304 | 305 | .following-right-bottom button { 306 | border-radius: 0; 307 | margin: 1px; 308 | background-color: #FFA500; 309 | color: #404040; 310 | border: 0px solid white; 311 | cursor: pointer; 312 | float: right; 313 | font-size: 1em; 314 | } 315 | 316 | .colorOne{ 317 | color: #404040; 318 | } 319 | 320 | .colorTwo{ 321 | color: #FFA500; 322 | } 323 | 324 | html .followed-user{ 325 | /*border: 2px solid white;*/ 326 | 327 | 328 | } 329 | .followed-user:hover { 330 | background: lightgrey; 331 | } 332 | .followed-user:after{ 333 | content: 'x'; 334 | } 335 | 336 | .auth-header-text { 337 | color: #404040; 338 | } 339 | 340 | .auth-container { 341 | border-top: 2px solid #404040; 342 | border-right: 2px solid #FFA500; 343 | background-color: #FFA500; 344 | height: 30%; 345 | } 346 | 347 | .auth-content{ 348 | text-align: right; 349 | width: 80%; 350 | padding: 2px; 351 | } 352 | 353 | .auth-content button{ 354 | cursor: pointer; 355 | font-family: helvetica, arial, sans-serif; 356 | -webkit-box-sizing: border-box; 357 | -moz-box-sizing: border-box; 358 | box-sizing: border-box; 359 | display: inline-block; 360 | border-radius: 0; 361 | border: 0px solid white; 362 | font-size: 1.15em; 363 | font-weight: bold; 364 | margin: 1px; 365 | padding: .2px .5em .2px .5em; 366 | background-color: #FFA500; 367 | color: #404040; 368 | } 369 | 370 | .auth-content input{ 371 | font-family: helvetica, arial, sans-serif; 372 | -webkit-box-sizing: border-box; 373 | -moz-box-sizing: border-box; 374 | box-sizing: border-box; 375 | display: block; 376 | border-radius: 0; 377 | border: 0px solid white; 378 | font-size: 1.15em; 379 | margin: 2px; 380 | padding: 0px 0px 0px .1em; 381 | background-color: #404040; 382 | background-color: white; 383 | color: #FFA500; 384 | color: gray; 385 | width: 100%; 386 | 387 | } 388 | 389 | .loggedInDiv{ 390 | display: inline-block; 391 | width: 100%; 392 | height: 100%; 393 | } 394 | 395 | .loggedOutDiv{ 396 | display: inline-block; 397 | margin-top: 1em; 398 | height: 100%; 399 | margin-right: 5%; 400 | width: 95%; 401 | } 402 | 403 | 404 | /*.hero-header { 405 | font-size: 50px; 406 | top: 270px; 407 | position: fixed; 408 | transform: rotate(-90deg); 409 | transform-origin: left top 0; 410 | left: 95%; 411 | margin: 0; 412 | }*/ 413 | 414 | .feed { 415 | display: inline-block; 416 | width: 69%; 417 | height: 100%; 418 | } 419 | 420 | .inside-feed{ 421 | display: inline-block; 422 | position: relative; 423 | width: 100%; 424 | height: 100%; 425 | overflow-y: scroll; 426 | } 427 | 428 | .inside-feed button{ 429 | border: 0px solid white; 430 | 431 | } 432 | 433 | .story-whole{ 434 | display: inline-block; 435 | width: 100%; 436 | } 437 | 438 | .story-whole-left{ 439 | display: inline-block; 440 | width: 132px; 441 | height: 100%; 442 | text-align: right; 443 | } 444 | 445 | .story-whole-right{ 446 | display: inline-block; 447 | width: 75%; 448 | 449 | /*min-width: 768px;*/ 450 | } 451 | 452 | .story{ 453 | margin-bottom: .6em; 454 | position: relative; 455 | } 456 | 457 | .comment { 458 | margin: 2px; 459 | padding: 1.5em; 460 | background-color: #E8E8E8; 461 | 462 | } 463 | 464 | .story-title { 465 | font-size: 1em; 466 | font-weight: 600; 467 | font-family: 'open sans', helvetica, arial, sans-serif; 468 | } 469 | 470 | .bottom-row { 471 | /*color: grey;*/ 472 | opacity: 0.4; 473 | font-size: .9em; 474 | } 475 | 476 | .top-row { 477 | margin-bottom: 20px; 478 | opacity: 0.35; 479 | font-size: .95em; 480 | } 481 | 482 | .points-bar { 483 | display: inline-block; 484 | background: #D8D8D8; 485 | /*background: black;*/ 486 | height: 1em; 487 | box-sizing: border-box; 488 | max-width: 132px; 489 | margin-right: .2em; 490 | } 491 | 492 | /*.active { 493 | width: 0; 494 | height: 0; 495 | border-top: 10px solid transparent; 496 | border-bottom: 10px solid transparent; 497 | border-left: 10px solid #585858; 498 | 499 | right: -5px; 500 | position: absolute; 501 | top: 38px; 502 | }*/ 503 | 504 | .source-url { 505 | opacity: 0.4; 506 | font-size: .9em; 507 | } 508 | 509 | .filterBar{ 510 | display: inline-block; 511 | width: 100%; 512 | background-color: #404040; 513 | padding: 2px; 514 | text-align: right; 515 | } 516 | 517 | .filterBarTitle{ 518 | display: inline-block; 519 | font-family: helvetica, arial, sans-serif; 520 | font-size: 1.2em; 521 | font-weight: bold; 522 | color: #FFA500; 523 | } 524 | 525 | html .filterBar input { 526 | font-family: helvetica, arial, sans-serif; 527 | -webkit-box-sizing: border-box; 528 | -moz-box-sizing: border-box; 529 | box-sizing: border-box; 530 | display: inline-block; 531 | border-radius: 0; 532 | border: 0px solid white; 533 | font-size: 1.15em; 534 | margin: 2px; 535 | padding: .15em 1em .15em 1em; 536 | background-color: #FFA500; 537 | color: #404040; 538 | } 539 | 540 | .more-button { 541 | display: block; 542 | width: 80%; 543 | margin-top: 1em; 544 | padding-top: 1em; 545 | padding-bottom: 1em; 546 | margin-right: auto; 547 | margin-left: auto; 548 | text-align: center; 549 | /*background: #e8e8e8;*/ 550 | border-top: 2px solid black; 551 | border-left: 2px solid black; 552 | border-right: 2px solid black; 553 | font-size: 1.6em; 554 | font-weight: bold; 555 | letter-spacing: -3px; 556 | opacity: 0.55; 557 | border-top-right-radius: 1em; 558 | border-top-left-radius: 1em; 559 | } 560 | .more-button:hover { 561 | opacity: 1; 562 | text-decoration: none; 563 | } 564 | 565 | .new-follow { 566 | 567 | } 568 | 569 | #signin { 570 | 571 | } 572 | 573 | .auth-info { 574 | } 575 | 576 | .logged-header { 577 | color: #404040; 578 | } 579 | 580 | 581 | 582 | .story-page{ 583 | position: relative; 584 | } 585 | 586 | .story-holder{ 587 | position: relative; 588 | } 589 | 590 | .story-holder a:hover { 591 | text-decoration: underline; 592 | } 593 | 594 | .refresh { 595 | position: absolute; 596 | display: inline-block; 597 | z-index: 999; 598 | left: 0; 599 | 600 | width: 40px; 601 | height: 40px; 602 | background: url(http://findicons.com/files/icons/2579/iphone_icons/40/reload.png); 603 | background-repeat: no-repeat; 604 | background-size: contain; 605 | opacity: 0.15; 606 | 607 | -webkit-transition: all 600ms ease-out; 608 | -moz-transition: all 600ms ease-out; 609 | -o-transition: all 600ms ease-out; 610 | -ms-transition: all 600ms ease-out; 611 | transition: all 600ms ease-out; 612 | } 613 | 614 | .refresh:hover { 615 | opacity: 0.7; 616 | } 617 | 618 | .clickDown{ 619 | -webkit-transform: scale(.94,.94); 620 | -moz-transform: scale(.94,.94); 621 | -ms-transform: scale(.94,.94); 622 | -o-transform: scale(.94,.94); 623 | transform: scale(.94,.94); 624 | } 625 | 626 | @media screen and (max-width: 1700px) { 627 | /*space for another column on the right here*/ 628 | } 629 | 630 | 631 | @media screen and (max-width: 1520px) { 632 | .points-bar{ 633 | 634 | } 635 | } 636 | 637 | @media screen and (max-width: 900px) { 638 | .points-bar{ 639 | display: none; 640 | } 641 | .story-whole-left{ 642 | display: none; 643 | } 644 | .story-whole-right{ 645 | width: 100%; 646 | margin: 2px; 647 | } 648 | } 649 | 650 | @media screen and (max-width: 768px) { 651 | .story-whole-right{ 652 | 653 | } 654 | 655 | html .leftSectionHeaderHolder{ 656 | display: block; 657 | text-align: right; 658 | } 659 | 660 | .leftSectionHeaderText{ 661 | font-weight: bold; 662 | text-align: right; 663 | font-size: 2em; 664 | letter-spacing: -.1em; 665 | margin-right: 2px; 666 | -webkit-transform: rotate(0)translate(0,0); 667 | -moz-transform: rotate(0)translate(0,0); 668 | -ms-transform: rotate(0)translate(0,0); 669 | -o-transform: rotate(0)translate(0,0); 670 | transform: rotate(0)translate(0,0); 671 | 672 | -webkit-transform-origin: left top; 673 | -moz-transform-origin: left top; 674 | -ms-transform-origin: left top; 675 | -o-transform-origin: left top; 676 | transform-origin: left top; 677 | } 678 | 679 | .leftSectionHolderContent{ 680 | display: block; 681 | position: relative; 682 | width: 100%; 683 | } 684 | 685 | html .tabs .leftSectionHeaderHolder{ 686 | height: 10%; 687 | } 688 | 689 | button.tab{ 690 | height: 22.5%; 691 | } 692 | 693 | } 694 | 695 | @media screen and (max-width: 700px) { 696 | html .left-container{ 697 | width: 100%; 698 | display: inline-block; 699 | height: 30%; 700 | max-width: 100%; 701 | } 702 | html .feed{ 703 | width: 100%; 704 | display: inline-block; 705 | height: 70%; 706 | } 707 | 708 | .leftSectionHolder{ 709 | display: inline-block; 710 | vertical-align: top; 711 | border: 0px solid white; 712 | height: 100%; 713 | width: 33.3333333%; 714 | } 715 | 716 | } 717 | 718 | 719 | 720 | 721 | -------------------------------------------------------------------------------- /public/lib/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | true 74 | 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 119 | 120 | 123 | 124 | 125 | 126 | 129 | 130 | 133 | 134 | 137 | 138 | 139 | 140 | 143 | 144 | 147 | 148 | 151 | 152 | 155 | 156 | 157 | 158 | 161 | 162 | 165 | 166 | 169 | 170 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 1431661522201 200 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 234 | 237 | 238 | 239 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /public/dist/production.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.authService', []) 2 | 3 | .factory('Auth', ["$http", "$location", "$window", function ($http, $location, $window) { 4 | var signin = function (user) { 5 | return $http({ 6 | method: 'POST', 7 | url: '/api/users/signin', 8 | data: user 9 | }) 10 | .then(function (resp) { 11 | return resp.data; 12 | }); 13 | }; 14 | 15 | var signup = function (user) { 16 | return $http({ 17 | method: 'POST', 18 | url: '/api/users/signup', 19 | data: user 20 | }) 21 | .then(function (resp) { 22 | return resp.data; 23 | }); 24 | }; 25 | 26 | var isAuth = function () { 27 | return !!$window.localStorage.getItem('com.hack'); 28 | }; 29 | 30 | var signout = function () { 31 | $window.localStorage.removeItem('com.hack'); 32 | }; 33 | 34 | 35 | return { 36 | signin: signin, 37 | signup: signup, 38 | isAuth: isAuth, 39 | signout: signout 40 | }; 41 | }]); 42 | // HOW OUR FOLLOWING SYSTEM WORKS: 43 | // We want users to be able to follow people before they even 44 | // log in, because who actually has time to decide on a story/password? 45 | 46 | // So, we do this by saving the users that they follow into localStorage. 47 | // On signup, we'll send the users string in localStorage to our server 48 | // which wil save them to a database. 49 | 50 | angular.module('hack.bookmarkService', []) 51 | 52 | .factory('Bookmarks', ["$http", "$window", function($http, $window) { 53 | var bookmarks = []; 54 | var user = $window.localStorage.getItem('com.hack'); 55 | 56 | var addBookmark = function(story){ 57 | 58 | var article = { 59 | points: story.points, 60 | url: story.url, 61 | title: story.title, 62 | author: story.author, 63 | created_at: story.created_at, 64 | objectID: story.objectID, 65 | num_comments: story.num_comments 66 | }; 67 | var data = { 68 | username: user, 69 | bookmark: article 70 | }; 71 | 72 | $http({ 73 | method: 'POST', 74 | url: '/api/bookmarks/addBookmark', 75 | data: data 76 | }); 77 | 78 | if (bookmarks.indexOf(article.objectID) === -1) { 79 | bookmarks.push(article.objectID); 80 | } 81 | }; 82 | 83 | var removeBookmark = function(story){ 84 | var article = { 85 | objectID: story.objectID 86 | }; 87 | 88 | var data = { 89 | username: user, 90 | bookmark: article 91 | }; 92 | 93 | $http({ 94 | method: 'POST', 95 | url: '/api/bookmarks/removeBookmark', 96 | data: data 97 | }); 98 | 99 | var splicePoint = bookmarks.indexOf(article.objectID); 100 | bookmarks.splice(splicePoint, 1); 101 | }; 102 | 103 | return { 104 | bookmarks: bookmarks, 105 | addBookmark: addBookmark, 106 | removeBookmark: removeBookmark 107 | } 108 | }]); 109 | 110 | // HOW OUR FOLLOWING SYSTEM WORKS: 111 | // We want users to be able to follow people before they even 112 | // log in, because who actually has time to decide on a username/password? 113 | 114 | // So, we do this by saving the users that they follow into localStorage. 115 | // On signup, we'll send the users string in localStorage to our server 116 | // which wil save them to a database. 117 | 118 | angular.module('hack.followService', []) 119 | 120 | .factory('Followers', ["$http", "$window", function($http, $window) { 121 | var following = []; 122 | 123 | var updateFollowing = function(){ 124 | var user = $window.localStorage.getItem('com.hack'); 125 | 126 | if(!!user){ 127 | var data = { 128 | username: user, 129 | following: localStorageUsers() 130 | }; 131 | 132 | $http({ 133 | method: 'POST', 134 | url: '/api/users/updateFollowing', 135 | data: data 136 | }); 137 | } 138 | }; 139 | 140 | var addFollower = function(username){ 141 | var localFollowing = localStorageUsers(); 142 | 143 | if (!localFollowing.includes(username) && following.indexOf(username) === -1) { 144 | localFollowing += ',' + username 145 | $window.localStorage.setItem('hfUsers', localFollowing); 146 | following.push(username); 147 | } 148 | 149 | // makes call to database to mirror our changes 150 | updateFollowing(); 151 | }; 152 | 153 | var removeFollower = function(username){ 154 | var localFollowing = localStorageUsers(); 155 | 156 | if (localFollowing.includes(username) && following.indexOf(username) > -1) { 157 | following.splice(following.indexOf(username), 1); 158 | 159 | localFollowing = localFollowing.split(','); 160 | localFollowing.splice(localFollowing.indexOf(username), 1).join(','); 161 | $window.localStorage.setItem('hfUsers', localFollowing); 162 | } 163 | 164 | // makes call to database to mirror our changes 165 | updateFollowing(); 166 | }; 167 | 168 | var localStorageUsers = function(){ 169 | return $window.localStorage.getItem('hfUsers'); 170 | } 171 | 172 | 173 | // this function takes the csv in localStorage and turns it into an array. 174 | // There are pointers pointing to the 'following' array. The 'following' array 175 | // is how our controllers listen for changes and dynamically update the DOM. 176 | // (because you can't listen to localStorage changes) 177 | var localToArr = function(){ 178 | if(!localStorageUsers()){ 179 | // If the person is a new visitor, set pg and sama as the default 180 | // people to follow. Kinda like Tom on MySpace. Except less creepy. 181 | $window.localStorage.setItem('hfUsers', 'pg,sama'); 182 | } 183 | 184 | var users = localStorageUsers().split(','); 185 | 186 | following.splice(0, following.length); 187 | following.push.apply(following, users); 188 | }; 189 | 190 | 191 | 192 | var init = function(){ 193 | localToArr(); 194 | }; 195 | 196 | init(); 197 | 198 | return { 199 | following: following, 200 | addFollower: addFollower, 201 | removeFollower: removeFollower, 202 | localToArr: localToArr 203 | } 204 | }]) 205 | 206 | angular.module('hack.linkService', []) 207 | 208 | .factory('Links', ["$window", "$http", "$interval", "Followers", "Bookmarks", function($window, $http, $interval, Followers, Bookmarks) { 209 | var personalStories = []; 210 | var topStories = []; 211 | var bookmarkStories = []; 212 | 213 | 214 | var topStoriesWithKeyword = []; 215 | 216 | var getTopStories = function() { 217 | console.log('getTopStories'); 218 | var url = '/api/cache/topStories' 219 | 220 | return $http({ 221 | method: 'GET', 222 | url: url 223 | }) 224 | .then(function(resp) { 225 | 226 | // Very important to not point topStories to a new array. 227 | // Instead, clear out the array, then push all the new 228 | // datum in place. There are pointers pointing to this array. 229 | topStories.splice(0, topStories.length); 230 | topStories.push.apply(topStories, resp.data); 231 | }); 232 | }; 233 | 234 | var getTopStoriesWithKeyword = function(keyword) { 235 | console.log('getTopStoriesWithKeyword'); 236 | var url = '/api/cache/topStoriesWithKeyword' 237 | 238 | return $http({ 239 | method: 'GET', 240 | url: url, 241 | params: {keyword: keyword} 242 | }) 243 | .then(function(resp) { 244 | console.log(resp); 245 | 246 | // Very important to not point topStories to a new array. 247 | // Instead, clear out the array, then push all the new 248 | // datum in place. There are pointers pointing to this array. 249 | topStoriesWithKeyword.splice(0, topStoriesWithKeyword.length); 250 | topStoriesWithKeyword.push.apply(topStoriesWithKeyword, resp.data); 251 | console.log(topStoriesWithKeyword); 252 | }); 253 | } 254 | 255 | var getPersonalStories = function(usernames){ 256 | var query = 'http://hn.algolia.com/api/v1/search_by_date?hitsPerPage=500&tagFilters=(story,comment),('; 257 | var csv = arrToCSV(usernames); 258 | 259 | query += csv + ')'; 260 | 261 | return $http({ 262 | method: 'GET', 263 | url: query 264 | }) 265 | .then(function(resp) { 266 | angular.forEach(resp.data.hits, function(item){ 267 | // HN Comments don't have a title. So flag them as a comment. 268 | // This will come in handy when we decide how to render each item. 269 | if(item.title === null){ 270 | item.isComment = true; 271 | } 272 | }); 273 | 274 | // Very important to not point personalStories to a new array. 275 | // Instead, clear out the array, then push all the new 276 | // datum in place. There are pointers pointing to this array. 277 | personalStories.splice(0, personalStories.length); 278 | personalStories.push.apply(personalStories, resp.data.hits); 279 | }); 280 | }; 281 | 282 | var getBookmarks = function(){ 283 | var user = $window.localStorage.getItem('com.hack'); 284 | 285 | var data = {username: user}; 286 | return $http({ 287 | method: 'POST', 288 | url: '/api/bookmarks/getBookmarks', 289 | data: data 290 | }) 291 | .then(function(resp) { 292 | bookmarkStories.splice(0, bookmarkStories.length); 293 | angular.forEach(resp.data, function (story) { 294 | bookmarkStories.push(story); 295 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 296 | Bookmarks.bookmarks.push(story.objectID); 297 | } 298 | }); 299 | }); 300 | }; 301 | 302 | var arrToCSV = function(arr){ 303 | var holder = []; 304 | 305 | for(var i = 0; i < arr.length; i++){ 306 | holder.push('author_' + arr[i]); 307 | } 308 | 309 | return holder.join(','); 310 | }; 311 | 312 | var init = function(){ 313 | getPersonalStories(Followers.following); 314 | 315 | $interval(function(){ 316 | getPersonalStories(Followers.following); 317 | getTopStories(); 318 | }, 300000); 319 | }; 320 | 321 | init(); 322 | 323 | return { 324 | getTopStories: getTopStories, 325 | getTopStoriesWithKeyword: getTopStoriesWithKeyword, 326 | getPersonalStories: getPersonalStories, 327 | personalStories: personalStories, 328 | topStories: topStories, 329 | topStoriesWithKeyword: topStoriesWithKeyword, 330 | getBookmarks: getBookmarks, 331 | bookmarkStories: bookmarkStories 332 | }; 333 | }]); 334 | 335 | 336 | 337 | angular.module('hack.auth', []) 338 | 339 | .controller('AuthController', ["$scope", "$window", "$location", "Auth", "Followers", "Bookmarks", 340 | function ($scope, $window, $location, Auth, Followers, Bookmarks) { 341 | 342 | $scope.user = {}; 343 | $scope.newUser = {}; 344 | $scope.loggedIn = Auth.isAuth(); 345 | 346 | $scope.signin = function () { 347 | Auth.signin($scope.user) 348 | .then(function (followers, bookmarks) { 349 | $window.localStorage.setItem('com.hack', $scope.user.username); 350 | $window.localStorage.setItem('hfUsers', followers); 351 | 352 | Followers.localToArr(); 353 | 354 | $scope.loggedIn = true; 355 | $scope.user = {}; 356 | }) 357 | .catch(function (error) { 358 | console.error(error); 359 | }); 360 | }; 361 | 362 | $scope.signup = function () { 363 | $scope.newUser.following = Followers.following.join(','); 364 | 365 | Auth.signup($scope.newUser) 366 | .then(function (data) { 367 | $window.localStorage.setItem('com.hack', $scope.newUser.username); 368 | 369 | $scope.loggedIn = true; 370 | $scope.newUser = {}; 371 | }) 372 | .catch(function (error) { 373 | console.error(error); 374 | }); 375 | }; 376 | 377 | $scope.logout = function () { 378 | Auth.signout(); 379 | $scope.loggedIn = false; 380 | } 381 | }]); 382 | 383 | angular.module('hack.currentlyFollowing', []) 384 | 385 | .controller('CurrentlyFollowingController', ["$scope", "Followers", function ($scope, Followers) { 386 | $scope.currentlyFollowing = Followers.following; 387 | 388 | $scope.unfollow = function(user){ 389 | Followers.removeFollower(user); 390 | }; 391 | 392 | $scope.follow = function(user){ 393 | Followers.addFollower(user); 394 | $scope.newFollow = ""; 395 | }; 396 | }]); 397 | 398 | angular.module('hack.personal', []) 399 | 400 | .controller('PersonalController', ["$scope", "$window", "Links", "Followers", "Auth", "Bookmarks", function ($scope, $window, Links, Followers, Auth, Bookmarks) { 401 | $scope.stories = Links.personalStories; 402 | $scope.users = Followers.following; 403 | $scope.perPage = 30; 404 | $scope.index = $scope.perPage; 405 | $scope.loggedIn = Auth.isAuth(); 406 | 407 | $scope.isBookmark = function(story) { 408 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 409 | return false; 410 | } else { 411 | return true; 412 | } 413 | }; 414 | 415 | $scope.addBookmark = function(story) { 416 | Bookmarks.addBookmark(story); 417 | }; 418 | 419 | $scope.removeBookmark = function(story) { 420 | Bookmarks.removeBookmark(story); 421 | }; 422 | 423 | var init = function(){ 424 | fetchUsers(); 425 | }; 426 | 427 | var fetchUsers = function(){ 428 | Links.getPersonalStories($scope.users); 429 | Links.getBookmarks(); 430 | }; 431 | 432 | init(); 433 | }]); 434 | 435 | 436 | angular.module('hack.bookmarks', []) 437 | 438 | .controller('BookmarksController', ["$scope", "$window", "Links", "Followers", "Bookmarks", "Auth", function ($scope, $window, Links, Followers, Bookmarks, Auth) { 439 | $scope.currentBookmarks = Bookmarks.bookmarks; 440 | $scope.loggedIn = Auth.isAuth(); 441 | $scope.stories = Links.bookmarkStories; 442 | $scope.perPage = 30; 443 | $scope.index = $scope.perPage; 444 | $scope.currentlyFollowing = Followers.following; 445 | 446 | $scope.addUser = function(username) { 447 | Followers.addFollower(username); 448 | }; 449 | 450 | $scope.isBookmark = function(story) { 451 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 452 | return false; 453 | } else { 454 | return true; 455 | } 456 | }; 457 | $scope.addBookmark = function(story) { 458 | Bookmarks.addBookmark(story); 459 | }; 460 | $scope.removeBookmark = function(story) { 461 | Bookmarks.removeBookmark(story); 462 | }; 463 | 464 | var init = function () { 465 | Links.getBookmarks(); 466 | }; 467 | init(); 468 | }]); 469 | angular.module('hack.tabs', []) 470 | 471 | .controller('TabsController', ["$scope", "$location", "$window", "Links", "Followers", function ($scope, $location, $window, Links, Followers) { 472 | // If a user refreshes when the location is '/personal', 473 | // it will stay on '/personal'. 474 | var hash = $window.location.hash.split('/')[1]; 475 | hash = !hash ? 'all' : hash; 476 | $scope.currentTab = hash; 477 | 478 | // What is angle? Don't worry. This just makes the 479 | // refresh button do a cool spin animation. We splurged. 480 | $scope.angle = 360; 481 | 482 | $scope.changeTab = function(newTab){ 483 | $scope.currentTab = newTab; 484 | $location.path(newTab); 485 | }; 486 | 487 | $scope.refresh = function(){ 488 | Links.getTopStories(); 489 | Links.getPersonalStories(Followers.following); 490 | Links.getBookmarks(); 491 | $scope.angle += 360; 492 | }; 493 | }]); 494 | 495 | angular.module('hack.topStories', []) 496 | 497 | .controller('TopStoriesController', ["$scope", "$window", "Links", "Followers", "Bookmarks", "Auth", function ($scope, $window, Links, Followers, Bookmarks, Auth) { 498 | angular.extend($scope, Links); 499 | $scope.stories = Links.topStories; 500 | $scope.perPage = 30; 501 | $scope.index = $scope.perPage; 502 | $scope.loggedIn = Auth.isAuth(); 503 | $scope.currentlyFollowing = Followers.following; 504 | 505 | $scope.getData = function() { 506 | Links.getTopStories(); 507 | }; 508 | 509 | $scope.addUser = function(username) { 510 | Followers.addFollower(username); 511 | }; 512 | 513 | $scope.isBookmark = function(story) { 514 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 515 | return false; 516 | } else { 517 | return true; 518 | } 519 | }; 520 | 521 | $scope.addBookmark = function(story) { 522 | Bookmarks.addBookmark(story); 523 | }; 524 | 525 | $scope.removeBookmark = function(story) { 526 | Bookmarks.removeBookmark(story); 527 | }; 528 | 529 | $scope.getData(); 530 | Links.getBookmarks(); 531 | }]); 532 | 533 | 534 | 535 | angular.module('hack.topStoriesWithKeyword', []) 536 | 537 | .controller('TopStoriesWithKeywordController', ["$scope", "$window", "Links", "Followers", "Bookmarks", "Auth", function ($scope, $window, Links, Followers, Bookmarks, Auth) { 538 | angular.extend($scope, Links); 539 | $scope.stories = Links.topStoriesWithKeyword; 540 | $scope.perPage = 30; 541 | $scope.index = $scope.perPage; 542 | $scope.loggedIn = Auth.isAuth(); 543 | $scope.keyword; 544 | $scope.checked = 'start'; 545 | 546 | // now i want to add a scope variable that is equal to the value of an input box 547 | // this might need to be a global variable so that its value can be set in the topStories page and still exist here 548 | // an alternative would be to click a link that takes us to the keyword page with the keyword initially set to '' 549 | 550 | $scope.currentlyFollowing = Followers.following; 551 | 552 | $scope.getData = function() { 553 | Links.getTopStoriesWithKeyword($scope.keyword); 554 | // $scope.checked = $scope.keyword; 555 | }; 556 | 557 | $scope.addUser = function(username) { 558 | Followers.addFollower(username); 559 | }; 560 | 561 | $scope.isBookmark = function(story) { 562 | if (Bookmarks.bookmarks.indexOf(story.objectID) === -1) { 563 | return false; 564 | } else { 565 | return true; 566 | } 567 | }; 568 | 569 | $scope.addBookmark = function(story) { 570 | Bookmarks.addBookmark(story); 571 | }; 572 | 573 | $scope.removeBookmark = function(story) { 574 | Bookmarks.removeBookmark(story); 575 | }; 576 | 577 | $scope.getData(''); // the argument here will eventually be set by the input box 578 | }]); 579 | 580 | 581 | angular.module('hack', [ 582 | 'hack.topStories', 583 | 'hack.topStoriesWithKeyword', 584 | 'hack.personal', 585 | 'hack.bookmarks', 586 | 'hack.bookmarkService', 587 | 'hack.currentlyFollowing', 588 | 'hack.linkService', 589 | 'hack.authService', 590 | 'hack.followService', 591 | 'hack.tabs', 592 | 'hack.auth', 593 | 'ngRoute' 594 | ]) 595 | 596 | .config(["$routeProvider", "$httpProvider", function($routeProvider, $httpProvider) { 597 | $routeProvider 598 | .when('/', { 599 | templateUrl: 'app/topStories/topStories.html', 600 | controller: 'TopStoriesController' 601 | }) 602 | .when('/personal', { 603 | templateUrl: 'app/personal/personal.html', 604 | controller: 'PersonalController' 605 | }) 606 | .when('/bookmarks', { 607 | templateUrl: 'app/bookmarks/bookmarks.html', 608 | controller: 'BookmarksController' 609 | }) 610 | .when('/keyword', { 611 | templateUrl: 'app/topStoriesWithKeyword/topStoriesWithKeyword.html', 612 | controller: 'TopStoriesWithKeywordController' 613 | }) 614 | .otherwise({ 615 | redirectTo: '/' 616 | }); 617 | }]) 618 | 619 | .filter('fromNow', function(){ 620 | return function(date){ 621 | var foo = 3; 622 | return humanized_time_span(new Date(date)); 623 | } 624 | }) 625 | 626 | .filter('htmlsafe', ['$sce', function ($sce) { 627 | return function (text) { 628 | return $sce.trustAsHtml(text); 629 | }; 630 | }]) 631 | 632 | .directive('rotate', function () { 633 | return { 634 | restrict: 'A', 635 | link: function (scope, element, attrs) { 636 | scope.$watch(attrs.degrees, function (rotateDegrees) { 637 | var r = 'rotate(' + rotateDegrees + 'deg)'; 638 | // console.log(r); 639 | element.css({ 640 | '-moz-transform': r, 641 | '-webkit-transform': r, 642 | '-o-transform': r, 643 | '-ms-transform': r, 644 | 'transform': r 645 | }); 646 | }); 647 | } 648 | } 649 | }); 650 | 651 | --------------------------------------------------------------------------------