├── Procfile
├── .gitattributes
├── README.md
├── app
├── robots.txt
├── styles
│ ├── _auth.scss
│ ├── _resets.scss
│ ├── main.scss
│ ├── _mixins.scss
│ ├── _variables.scss
│ ├── _settings.scss
│ ├── _globals.scss
│ ├── _chart.scss
│ ├── _modal.scss
│ ├── _layout.scss
│ ├── _tooltip.scss
│ └── _storyPanel.scss
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-96x96.png
├── mstile-144x144.png
├── mstile-150x150.png
├── mstile-310x150.png
├── mstile-310x310.png
├── mstile-70x70.png
├── apple-touch-icon.png
├── favicon-160x160.png
├── favicon-196x196.png
├── apple-touch-icon-114x114.png
├── apple-touch-icon-120x120.png
├── apple-touch-icon-144x144.png
├── apple-touch-icon-152x152.png
├── apple-touch-icon-57x57.png
├── apple-touch-icon-60x60.png
├── apple-touch-icon-72x72.png
├── apple-touch-icon-76x76.png
├── apple-touch-icon-precomposed.png
├── test.js
├── browserconfig.xml
├── scripts
│ ├── app.js
│ ├── init.js
│ ├── nav.js
│ ├── utils.js
│ ├── main.js
│ ├── favs.js
│ ├── layout.js
│ ├── actions.js
│ ├── auth.js
│ ├── storyModel.js
│ ├── events.js
│ ├── settings.js
│ ├── storyPanel.js
│ ├── comments.js
│ ├── data.js
│ └── chart.js
├── test.html
├── images
│ └── gearIcon.svg
├── 404.html
└── about.html
├── dist
├── robots.txt
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-96x96.png
├── mstile-70x70.png
├── favicon-160x160.png
├── favicon-196x196.png
├── mstile-144x144.png
├── mstile-150x150.png
├── mstile-310x150.png
├── mstile-310x310.png
├── apple-touch-icon.png
├── apple-touch-icon-57x57.png
├── apple-touch-icon-60x60.png
├── apple-touch-icon-72x72.png
├── apple-touch-icon-76x76.png
├── apple-touch-icon-114x114.png
├── apple-touch-icon-120x120.png
├── apple-touch-icon-144x144.png
├── apple-touch-icon-152x152.png
├── apple-touch-icon-precomposed.png
├── test.js
├── browserconfig.xml
├── test.html
├── 404.html
├── about.html
└── styles
│ └── main-dd5e0326.css
├── .bowerrc
├── test
├── .bowerrc
├── bower.json
├── spec
│ └── test.js
└── index.html
├── artifacts
├── icon.png
├── icon.psd
├── reddit7.png
├── facebook43.png
├── google109.png
├── twitter35.png
├── reddit-alien.jpeg
├── Bubble Reader Design.pptx
└── star.svg
├── .gitignore
├── strongloop.json
├── server
├── models
│ ├── Token.model.js
│ ├── Story.model.js
│ └── User.model.js
├── readability.js
├── new-hxncrawler.js
├── config.js
├── utils.js
├── server.js
├── routes.js
├── workers.js
├── controllers
│ ├── user.controller.js
│ └── story.controller.js
├── rdtCrawler.js
├── hxnCrawler.js
└── auth.js
├── bower.json
├── .jshintrc
├── start.js
├── .editorconfig
├── LICENSE
├── package.json
├── npm-debug.log
└── gulpfile.js
/Procfile:
--------------------------------------------------------------------------------
1 | web: node start
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #News Bubbles
2 | Shows news. As bubbles.
--------------------------------------------------------------------------------
/app/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org/
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/app/styles/_auth.scss:
--------------------------------------------------------------------------------
1 | .modal-wrapper.settings {
2 |
3 | }
--------------------------------------------------------------------------------
/dist/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org/
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "app/bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/test/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/favicon.ico
--------------------------------------------------------------------------------
/artifacts/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/icon.png
--------------------------------------------------------------------------------
/artifacts/icon.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/icon.psd
--------------------------------------------------------------------------------
/app/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/favicon-16x16.png
--------------------------------------------------------------------------------
/app/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/favicon-32x32.png
--------------------------------------------------------------------------------
/app/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/favicon-96x96.png
--------------------------------------------------------------------------------
/app/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/mstile-144x144.png
--------------------------------------------------------------------------------
/app/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/mstile-150x150.png
--------------------------------------------------------------------------------
/app/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/mstile-310x150.png
--------------------------------------------------------------------------------
/app/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/mstile-310x310.png
--------------------------------------------------------------------------------
/app/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/mstile-70x70.png
--------------------------------------------------------------------------------
/artifacts/reddit7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/reddit7.png
--------------------------------------------------------------------------------
/dist/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/favicon-16x16.png
--------------------------------------------------------------------------------
/dist/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/favicon-32x32.png
--------------------------------------------------------------------------------
/dist/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/favicon-96x96.png
--------------------------------------------------------------------------------
/dist/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/mstile-70x70.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .tmp
3 | .idea
4 | .sass-cache
5 | app/bower_components
6 | test/bower_components
7 |
--------------------------------------------------------------------------------
/app/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon.png
--------------------------------------------------------------------------------
/app/favicon-160x160.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/favicon-160x160.png
--------------------------------------------------------------------------------
/app/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/favicon-196x196.png
--------------------------------------------------------------------------------
/artifacts/facebook43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/facebook43.png
--------------------------------------------------------------------------------
/artifacts/google109.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/google109.png
--------------------------------------------------------------------------------
/artifacts/twitter35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/twitter35.png
--------------------------------------------------------------------------------
/dist/favicon-160x160.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/favicon-160x160.png
--------------------------------------------------------------------------------
/dist/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/favicon-196x196.png
--------------------------------------------------------------------------------
/dist/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/mstile-144x144.png
--------------------------------------------------------------------------------
/dist/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/mstile-150x150.png
--------------------------------------------------------------------------------
/dist/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/mstile-310x150.png
--------------------------------------------------------------------------------
/dist/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/mstile-310x310.png
--------------------------------------------------------------------------------
/artifacts/reddit-alien.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/reddit-alien.jpeg
--------------------------------------------------------------------------------
/dist/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/dist/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/app/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/app/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/artifacts/Bubble Reader Design.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/artifacts/Bubble Reader Design.pptx
--------------------------------------------------------------------------------
/dist/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidgilbertson/news-bubbles/HEAD/dist/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/test/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hb",
3 | "private": true,
4 | "dependencies": {
5 | "chai": "~1.8.0",
6 | "mocha": "~1.14.0"
7 | },
8 | "devDependencies": {}
9 | }
10 |
--------------------------------------------------------------------------------
/app/styles/_resets.scss:
--------------------------------------------------------------------------------
1 | *, *:before, *:after {
2 | box-sizing: border-box;
3 | }
4 |
5 | hr {
6 | display: block;
7 | height: 1px;
8 | border: 0;
9 | border-top: 1px solid #ccc;
10 | margin: 1em 0;
11 | padding: 0;
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/strongloop.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "17c200c04bd6cf974f3e7c697abaf67e",
3 | "userId": 100006775,
4 | "name": "David Gilbertson",
5 | "userKey": "17c200c04bd6cf974f3e7c697abaf67e",
6 | "email": "gilbertson.david@gmail.com",
7 | "created": "2014-09-20T05:44:53.000Z"
8 | }
9 |
--------------------------------------------------------------------------------
/app/test.js:
--------------------------------------------------------------------------------
1 | var circle1 = d3.select('#circle-1');
2 | var circle2 = d3.select('#circle-2');
3 | var circle3 = d3.select('#circle-3');
4 |
5 | circle3.node(0).parentNode.insertBefore(circle3.node(0), circle3.node(0).parentNode.firstChild);
6 |
7 | console.log('circle test loaded');
--------------------------------------------------------------------------------
/dist/test.js:
--------------------------------------------------------------------------------
1 | var circle1 = d3.select('#circle-1');
2 | var circle2 = d3.select('#circle-2');
3 | var circle3 = d3.select('#circle-3');
4 |
5 | circle3.node(0).parentNode.insertBefore(circle3.node(0), circle3.node(0).parentNode.firstChild);
6 |
7 | console.log('circle test loaded');
--------------------------------------------------------------------------------
/server/models/Token.model.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var mongoose = require('mongoose');
3 |
4 | var tokenSchema = mongoose.Schema({
5 | token: String,
6 | userId: String
7 | });
8 |
9 | var Token = mongoose.model('token', tokenSchema);
10 |
11 | module.exports = Token;
--------------------------------------------------------------------------------
/app/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import
2 | '../bower_components/normalize-scss/_normalize.scss',
3 | 'resets',
4 | 'variables',
5 | 'mixins',
6 | 'layout',
7 | 'globals',
8 |
9 | 'modal',
10 | 'settings',
11 | 'auth',
12 |
13 | 'tooltip',
14 | 'chart',
15 | 'storyPanel'
16 | ;
17 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hb",
3 | "private": true,
4 | "dependencies": {
5 | "jquery": "~1.11.0",
6 | "d3": "~3.4.11",
7 | "knockout": "~3.2.0",
8 | "moment": "~2.8.3",
9 | "firebase": "~1.1.0"
10 | },
11 | "devDependencies": {
12 | "normalize-scss": "~3.0.1"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin shadow {
2 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
3 | }
4 | @mixin no-style-button {
5 | border: none;
6 | outline: none;
7 | padding: 0;
8 | margin: 0;
9 | background: white;
10 | }
11 | @mixin no-style-ul {
12 | list-style-type: none;
13 | margin: 0;
14 | padding: 0;
15 | }
--------------------------------------------------------------------------------
/test/spec/test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it */
2 |
3 | (function () {
4 | 'use strict';
5 |
6 | describe('Give it some context', function () {
7 | describe('maybe a bit more context here', function () {
8 | it('should run here few assertions', function () {
9 |
10 | });
11 | });
12 | });
13 | })();
14 |
--------------------------------------------------------------------------------
/app/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #da532c
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/dist/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #da532c
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/scripts/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.App = (function() {
5 | var App = {};
6 |
7 | App.maxiTooltipVis = ko.observable(false);
8 | App.user = NB.Auth.userModel;
9 | App.settings = NB.Settings;
10 | App.nav = NB.Nav.navModel;
11 | App.storyModel = NB.StoryModel.storyModel;
12 |
13 | App.view = {
14 | showMaxiTooltip: ko.observable(false)
15 | };
16 |
17 |
18 | function init() {
19 | ko.applyBindings(App, document.body);
20 |
21 | }
22 |
23 | init();
24 | return App;
25 |
26 | })();
27 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": false,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 4,
11 | "latedef": true,
12 | "laxcomma": true,
13 | "newcap": true,
14 | "noarg": true,
15 | "quotmark": "single",
16 | "undef": true,
17 | "unused": true,
18 | "strict": true,
19 | "trailing": true,
20 | "smarttabs": true,
21 | "jquery": true,
22 | "predef": ["$", "d3", "io", "ko", "moment", "socket", "RegExp", "config"]
23 | }
24 |
--------------------------------------------------------------------------------
/start.js:
--------------------------------------------------------------------------------
1 | //start.js is kicked off by Heroku.
2 | //it creates an express app, serves out of dist and passes app on to server/server.js
3 | //For dev, gulp is creating an express app instance, serving out of app and .tmp and passing it to server.js
4 | //The magic happens in server.js and beyond.
5 |
6 | var path = require('path')
7 | , express = require('express')
8 | , app = express()
9 | ;
10 |
11 |
12 | app.use(require('compression')());
13 |
14 | app.use(express.static('dist'));
15 |
16 | var server = require(path.join(__dirname, 'server', 'server.js'));
17 |
18 | server.start(app);
19 |
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 4
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
23 | [package.json]
24 | indent_style = space
25 | indent_size = 2
26 |
27 | [bower.json]
28 | indent_style = space
29 | indent_size = 2
30 |
--------------------------------------------------------------------------------
/app/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/dist/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/server/readability.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var request = require('request')
3 | , readabilityUrl = 'https://readability.com/api/content/v1/parser'
4 | , readabilityToken = '10b42cde5b1b55ef7c219a98834c8c823a2b0cc3'
5 | ;
6 |
7 | //use the readability API to scrape a web page and return the content object
8 | module.exports = function(req, res) {
9 | var fullUrl = readabilityUrl + '?url=' + encodeURIComponent(req.params.url) + '&token=' + readabilityToken;
10 |
11 | request.get({url: fullUrl, json: true}, function (err, req, body) {
12 | if (err) {
13 | res.json({error: 'error'});
14 | } else {
15 | res.json(body);
16 | }
17 | });
18 |
19 | };
20 |
21 |
--------------------------------------------------------------------------------
/app/styles/_variables.scss:
--------------------------------------------------------------------------------
1 |
2 | /* -- Colors -- */
3 |
4 | $asbestos: #7f8c8d;
5 | $aliezarin: #e74c3c;
6 | $green-sea: #16a085;
7 | $nephritis: #27ae60;
8 | $orange: #f39c12;
9 | $pumpkin: #d35400;
10 | $belizeHole: #2980b9;
11 |
12 |
13 | $primary-color: $aliezarin;
14 |
15 | $header-color: $primary-color;
16 | $grabber-color: $primary-color;
17 |
18 | $resizer-width: 24px; //matches HB.RESIZER_WIDTH
19 |
20 | $story-color: #2980b9;
21 | $ask-color: $nephritis;
22 | $show-color: $pumpkin;
23 |
24 | $move-dur: 200ms; //Should be matched by JS global
25 |
26 |
27 | /* -- TYPOGRAPHY -- */
28 | $font-family: 'Roboto', sans-serif;
29 | $col-text-primary: $primary-color;
30 | $col-text-light: #ddd;
31 |
32 |
33 | /* -- Dimensions -- */
34 | $header-height: 50px;
35 | $break-sm: 400px;
36 | $break-md: 700px;
37 |
--------------------------------------------------------------------------------
/artifacts/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | ]>
7 |
12 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Mocha Spec Runner
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/scripts/init.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 | console.time('initialize app');
4 |
5 | //Everything here runs before anything else is initialized
6 |
7 | //Constants
8 | NB.DUR_FAST = 200; //should match _variables.scss duration variable
9 | NB.DUR_SLOW = 2000;
10 | NB.RESIZER_WIDTH = 24;
11 | NB.splitPos = 0;
12 |
13 | if (!!document.location.host.match(/localhost/)) {
14 | NB.IS_LOCALHOST = true;
15 | } else {
16 | NB.IS_LOCALHOST = false;
17 | }
18 |
19 | NB.hasTouch = false;
20 | NB.oldestStory = Infinity;
21 |
22 |
23 | var targetLocalStorageVersion = 1; //increment this to wipe the localstorage for older versions
24 | var lsVersion = localStorage.v ? localStorage.v : 0;
25 | if (lsVersion < targetLocalStorageVersion) {
26 | console.log('Clearing local storage.');
27 | localStorage.clear();
28 | localStorage.v = targetLocalStorageVersion;
29 | }
--------------------------------------------------------------------------------
/app/scripts/nav.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var NB = NB || {};
4 |
5 | NB.Nav = (function() {
6 | var Nav = {}
7 | , currentSource
8 | ;
9 |
10 | function init() {
11 | currentSource = NB.Settings.getSetting('source');
12 | Nav.navModel.currentSource(currentSource);
13 | }
14 |
15 | Nav.navModel = {
16 | currentSource: ko.observable(currentSource)
17 | };
18 |
19 | Nav.navigate = function(newSource) {
20 | NB.Layout.hideStoryPanel();
21 | NB.StoryModel.clear();
22 | // console.log('Going to navigate to', newSource);
23 | Nav.navModel.currentSource(newSource);
24 | NB.Settings.setSetting('source', newSource);
25 |
26 | NB.Chart.reset();
27 | NB.Data.getData();
28 |
29 | //TODO: less dumb way to do this?
30 | var body = d3.select('body');
31 | body.classed('rdt', newSource === 'rdt');
32 | body.classed('hxn', newSource === 'hxn');
33 | body.classed('fav', newSource === 'fav');
34 | };
35 |
36 | init();
37 | return Nav;
38 | })();
--------------------------------------------------------------------------------
/app/scripts/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var NB = NB || {};
4 |
5 | NB.Utils = (function() {
6 | var Utils = {};
7 |
8 | Utils.constrain = function(low, val, high) {
9 | val = Math.max(low, val);
10 | val = Math.min(val, high);
11 | return val;
12 | };
13 |
14 | Utils.unescape = function(str) {
15 | var unEscapeMap = {
16 | '&': '&',
17 | '<': '<',
18 | '>': '>',
19 | '"': '"',
20 | ''': "'",
21 | '`': '`',
22 | '/': '/'
23 | };
24 |
25 | var escaper = function(match) {
26 | return unEscapeMap[match];
27 | };
28 |
29 | var keysAsString = Object.keys(unEscapeMap).join('|');
30 | var source = '(?:' + keysAsString + ')';
31 | var testRegexp = RegExp(source);
32 | var replaceRegexp = RegExp(source, 'g');
33 |
34 | str = str == null ? '' : '' + str;
35 | return testRegexp.test(str) ? str.replace(replaceRegexp, escaper) : str;
36 |
37 | };
38 |
39 | return Utils;
40 | })();
--------------------------------------------------------------------------------
/app/styles/_settings.scss:
--------------------------------------------------------------------------------
1 | .modal-wrapper.settings {
2 | .footer .about-link {
3 | line-height: 40px;
4 | }
5 | .option {
6 | margin-bottom: 10px;
7 |
8 | p {
9 | margin: 0;
10 | padding-bottom: 5px;
11 | }
12 | label, input {
13 | cursor: pointer;
14 | }
15 | .inline-input {
16 | transition: 100ms;
17 | border: none;
18 | padding-left: 15px;
19 | border-bottom: 1px solid #ddd;
20 | text-align: center;
21 | /* background: #eee; */
22 | width: 80px;
23 | &:focus {
24 | width: 80px;
25 | outline: none;
26 | }
27 | }
28 | .color-list {
29 | @include no-style-ul;
30 |
31 | li {
32 | padding-bottom: 4px;
33 | }
34 | .dot {
35 | display: inline-block;
36 | height: 20px;
37 | width: 20px;
38 | border-radius: 50%;
39 | }
40 | .text {
41 | vertical-align: 4px;
42 | padding-left: 9px;
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 David Gilbertson
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/models/Story.model.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var mongoose = require('mongoose');
3 |
4 | var storySchema = mongoose.Schema({
5 | id: String, //source + id (e.g. 'hn-123456')
6 | source: String, //e.g. hn
7 | sourceId: String, //e.g. 123456
8 | modifiedDate: Date,
9 | name: String,
10 | desc: String,
11 | postDate: Date,
12 | postDateSeconds: Number,
13 | url: String,
14 | sourceUrl: String,
15 | authorUrl: String,
16 | category: String, //e.g. askHN, imgur, askReddit, nytimes.com
17 | commentCount: Number,
18 | score: Number,
19 | author: String,
20 | thumbnail: String,
21 | rdt: {}, //reddit specific stuff
22 | hxn: {}, //hacker news specific stuff
23 | twt: {}, //twitter specific stuff
24 | tbl: {}, //tumblr specific stuff
25 | history: [
26 | {
27 | dateTime: Date,
28 | commentCount: Number,
29 | score: Number
30 | }
31 | ]
32 | });
33 |
34 | storySchema.set('autoIndex', false); //redundant since I've removed indexes, but there as a net
35 |
36 | var Story = mongoose.model('Story', storySchema);
37 |
38 | exports.Story = Story;
--------------------------------------------------------------------------------
/app/scripts/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.main = (function() {
5 | NB.splitPos = document.body.offsetWidth - NB.RESIZER_WIDTH;
6 | NB.Layout.render();
7 | NB.Layout.init();
8 |
9 | var src = NB.Settings.getSetting('source') || 'rdt'; //this should never be empty, but 'rd' is there for the fun of it.
10 | var minScore = NB.Settings.getSetting(src + 'MinScore');
11 |
12 |
13 | d3.select('body').classed(src, true);
14 |
15 | //On page load, use the APIs directly from the client to get a fresh batch of results
16 | //The server will be emitting new/changed stories as they become available.
17 | NB.Data.getData(src, minScore);
18 |
19 | // ko.applyBindings(NB.StoryModel.tooltipStory, document.getElementById('story-tooltip'));
20 | // ko.applyBindings(NB.StoryModel.panelStory, document.getElementById('story-panel'));
21 | // ko.applyBindings(NB.Nav.navModel, document.getElementById('news-sources'));
22 |
23 |
24 | //Two approaches to touch detection
25 | // if (!('ontouchstart' in window) && !(window.DocumentTouch && document instanceof DocumentTouch)) {
26 | // d3.select('body').classed('no-touch', true);
27 | // NB.hasTouch = false;
28 | // }
29 |
30 | var onFirstTouch = function() {
31 | document.body.classList.remove('no-touch');
32 | NB.hasTouch = true;
33 | document.body.removeEventListener('touchstart', onFirstTouch);
34 | };
35 | document.body.addEventListener('touchstart', onFirstTouch);
36 |
37 |
38 |
39 | })();
--------------------------------------------------------------------------------
/server/new-hxncrawler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path')
4 | , request = require('request')
5 | , Firebase = require('firebase')
6 | , storyController = require(path.join(__dirname, 'controllers', 'story.controller'))
7 | , utils = require(path.join(__dirname, 'utils'))
8 | , devLog = utils.devLog
9 | , prodLog = utils.prodLog
10 | , fbBaseUrl = 'https://hacker-news.firebaseio.com/v0'
11 | ;
12 |
13 |
14 |
15 | function getByIdFromFirebase(id) {
16 | var requestOptions = {
17 | url: fbBaseUrl + '/item/' + id + '.json',
18 | json: true
19 | };
20 | request.get(requestOptions, function(err, res, data) {
21 | if (err) {
22 | return prodLog('Error getting details from firebase:', err);
23 | } else if (data && data.type !== 'story') {
24 | return; //doing nothing with comments for now.
25 | } else {
26 | return storyController.upsertFbHxnStory(data);
27 | }
28 | });
29 | }
30 |
31 | function start() {
32 | var fb = new Firebase(fbBaseUrl + '/updates');
33 | var newStoryList = [];
34 |
35 | fb.on('value', function (snapshot) {
36 | try {
37 | if (!snapshot.val() || !snapshot.val().items) { return; }
38 |
39 | snapshot.val().items.forEach(function(storyId) {
40 | getByIdFromFirebase(storyId);
41 | });
42 |
43 | } catch (err) {
44 | prodLog('Error in Firebase listener:', err);
45 | }
46 | }, function(err) {
47 | prodLog('Error in Firebase listener:', err);
48 | });
49 |
50 | }
51 |
52 | exports.start = start;
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var prodUrl = 'http://www.bubblereader.com';
4 | var devUrl = 'http://local.bubblereader.com';
5 |
6 | var prodConfig = {
7 | // baseUrl: 'http://www.bubblereader.com',
8 | db: {
9 | port: process.env.PORT,
10 | conn: process.env.MONGOLAB_URL
11 | },
12 | auth: {
13 | facebook: {
14 | clientId: '833772886647232',
15 | secret: '862d4c22a83572793c7214d798afe5f3',
16 | callbackUrl: prodUrl + '/auth/facebook/callback'
17 | },
18 | reddit: {
19 | clientId: '1_v-tNQj16e7Sg',
20 | secret: '_yzoDtfgvzMlrFK56mLFlPt6oY4',
21 | callbackUrl: prodUrl + '/auth/reddit/callback'
22 | }
23 | },
24 | nodetime: {
25 | accountKey: process.env.NODETIME_ACCOUNT_KEY,
26 | appName: 'News Bubbles' // optional
27 | }
28 | };
29 |
30 |
31 | var devConfig = {
32 | // baseUrl: 'http://local.bubblereader.com',
33 | db: {
34 | port: 9000,
35 | conn: 'mongodb://localhost/news_bubbles'
36 | },
37 | auth: {
38 | facebook: {
39 | clientId: prodConfig.auth.facebook.clientId,
40 | secret: prodConfig.auth.facebook.secret,
41 | callbackUrl: devUrl + '/auth/facebook/callback'
42 | },
43 | reddit: {
44 | clientId: '4tPkcfJiC76--w',
45 | secret: 'hWWKy8NNYeiP8ZFXadhS204t4a4',
46 | callbackUrl: devUrl + '/auth/reddit/callback'
47 | }
48 | },
49 | nodetime: {
50 | accountKey: '05d915a7339098057141246ef49ab77a3c5bd013',
51 | appName: 'News Bubbles Dev'
52 | }
53 | };
54 |
55 |
56 | module.exports = {
57 | prod: prodConfig,
58 | dev: devConfig
59 | };
60 |
--------------------------------------------------------------------------------
/app/styles/_globals.scss:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | overflow: hidden;
4 | min-width: 300px;
5 | }
6 | body {
7 | font-family: $font-family;
8 | font-weight: 300;
9 | letter-spacing: 0.3px;
10 | color: #333;
11 | background-image: linear-gradient(0deg, #000, #1C2732);
12 | }
13 | h1, h2, h3, h4, h5, h6 {
14 | font-weight: 300;
15 | /* color: $header-color; */
16 | }
17 | a {
18 | /* text-decoration: none; */
19 | /* color: darken($header-color, 10%); */
20 | color: $header-color;
21 | }
22 | a:hover {
23 | color: darken($header-color, 50%);
24 | text-decoration: underline;
25 | }
26 | p {
27 | font-weight: 300;
28 | }
29 | figure {
30 | margin: 0;
31 | }
32 |
33 | button.nil-style {
34 | background: none;
35 | border: none;
36 | outline: none;
37 | padding: 0;
38 | &:focus {
39 | outline: none;
40 | }
41 | }
42 |
43 | #test-modal {
44 | position: fixed;
45 | width: 500px;
46 | height: 500px;
47 | left: 100px;
48 | top: 100px;
49 | background: white;
50 | z-index: 1;
51 | padding: 5px;
52 | }
53 |
54 | //Logic for setting font faces like so:
55 | //http://www.smashingmagazine.com/2013/02/14/setting-weights-and-styles-at-font-face-declaration/
56 | @font-face {
57 | font-family: 'Roboto';
58 | font-weight: 400;
59 | scr: local('Roboto')
60 | , url(http://fonts.gstatic.com/s/roboto/v13/CWB0XYA8bzo0kSThX0UTuA.woff2);
61 | }
62 | @font-face {
63 | font-family: 'RobotoLight';
64 | font-weight: 300;
65 | scr: local('Roboto-Light')
66 | , local('Roboto Light')
67 | , url(http://fonts.gstatic.com/s/roboto/v13/Hgo13k-tfSpn0qi1SFdUfVtXRa8TVwTICgirnJhmVJw.woff2);
68 | }
--------------------------------------------------------------------------------
/server/models/User.model.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var mongoose = require('mongoose');
3 |
4 | var userSchema = mongoose.Schema({
5 | providerId: String, //TODO delete
6 | provider: String, //e.g. reddit, facebook, gooogle, etc.
7 | username: String, //TODO delete
8 | password: String, //TODO delete
9 | facebook: {
10 | id: String,
11 | token: String
12 | },
13 | reddit: {
14 | id: String,
15 | token: String,
16 | refreshToken: String
17 | },
18 | name: {
19 | first: String,
20 | middle: String,
21 | last: String,
22 | display: String
23 | },
24 | displayName: String,
25 | email: String,
26 | settings: {
27 | hitLimit: {type: Number, default: 80},
28 | hxnMinScore: {type: Number, default: 5},
29 | rdtMinScore: {type: Number, default: 500},
30 | clickAction: {type: String, default: 'storyPanel'},
31 | rightClickAction: {type: String, default: 'toggleRead'},
32 | source: {type: String, default: 'rdt'}
33 | },
34 | readList: [], //array of sourceIds for stories that have been read
35 | favs: [], //array of sourceIds for stories that are favourites
36 | stories: [
37 | {
38 | storyId: String, //the mongo ID of the story
39 | fav: Boolean,
40 | read: Boolean,
41 | vote: String // undefined || 'up' || 'down'
42 | }
43 | ] //array of stories that the user has had some interaction with.
44 | });
45 |
46 | var User = mongoose.model('user', userSchema);
47 |
48 | module.exports = User;
--------------------------------------------------------------------------------
/app/styles/_chart.scss:
--------------------------------------------------------------------------------
1 | .chart {
2 | &-wrapper {
3 | transition: width $move-dur * 4; //turned off while dragging
4 | position: absolute;
5 | left: 0px;
6 | width: 100%; //gets reset on load
7 | height: 100%;
8 | .overlay {
9 | pointer-events: all;
10 | fill: none;
11 | cursor: ew-resize;
12 | }
13 | .stripes {
14 | opacity: 0;
15 | .stripe-odd {
16 | fill: white;
17 | }
18 | .stripe-even {
19 | fill: none;
20 | }
21 |
22 | }
23 | }
24 | &-axis {
25 | shape-rendering: crispEdges;
26 | text {
27 | fill: white;
28 | opacity: 0.4;
29 | font-size: 12px;
30 | letter-spacing: 1px;
31 | }
32 | path {
33 | fill: #777;
34 | stroke-width: 1;
35 | }
36 | }
37 | &-legend {
38 | display: none;
39 | text {
40 | font-size: 14px;
41 | stroke: none;
42 | fill: $col-text-light;
43 | }
44 | }
45 | }
46 |
47 | .story-circle {
48 | transition: 1000ms;
49 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
50 | -webkit-tap-highlight-color: transparent;
51 | /* opacity: 0.7; */
52 | fill-opacity: 0.7;
53 | stroke-opacity: 0.7;
54 | /* transition: 200ms; */
55 | cursor: pointer;
56 | /* .chart-plot-area:not(.moving) { */
57 | /* opacity: 0.7; */
58 | /* } */
59 | body:not(.fav) &.read { //yeah that's not confusing AT ALL
60 | fill-opacity: 0.2;
61 | stroke-opacity: 0.6;
62 | }
63 | &.selected {
64 | stroke-width: 2px;
65 | stroke-opacity: 1;
66 | stroke: #eee;
67 | }
68 | }
69 | .tick {
70 | line {
71 | stroke: white;
72 | opacity: 0.05;
73 | }
74 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bubblereader",
3 | "version": "0.4.10",
4 | "dependencies": {
5 | "body-parser": "^1.9.0",
6 | "compression": "^1.1.0",
7 | "cookie-parser": "^1.3.3",
8 | "cookie-session": "^1.0.2",
9 | "express": "^4.8.4",
10 | "express-session": "^1.8.2",
11 | "firebase": "^1.1.0",
12 | "mongoose": "4.0.1",
13 | "nodetime": "^0.8.15",
14 | "passport": "^0.2.1",
15 | "passport-facebook": "^1.0.3",
16 | "passport-local": "^1.0.0",
17 | "passport-reddit": "^0.2.4",
18 | "passport-remember-me": "0.0.1",
19 | "request": "^2.40.0",
20 | "socket.io": "^1.0.6",
21 | "webkit-devtools-agent": "^0.3.1"
22 | },
23 | "devDependencies": {
24 | "connect": "^2.14.4",
25 | "connect-livereload": "^0.4.0",
26 | "gulp": "^3.6.0",
27 | "gulp-autoprefixer": "^0.0.7",
28 | "gulp-bower-files": "^0.2.1",
29 | "gulp-cache": "^0.1.1",
30 | "gulp-clean": "^0.2.4",
31 | "gulp-csso": "^0.2.6",
32 | "gulp-filter": "^0.4.1",
33 | "gulp-flatten": "^0.0.2",
34 | "gulp-imagemin": "^0.5.0",
35 | "gulp-jshint": "^1.5.3",
36 | "gulp-livereload": "^1.2.0",
37 | "gulp-load-plugins": "^0.5.0",
38 | "gulp-minify-css": "^0.3.7",
39 | "gulp-nodemon": "^1.0.4",
40 | "gulp-rev": "^1.1.0",
41 | "gulp-rev-replace": "^0.3.1",
42 | "gulp-ruby-sass": "^0.7.1",
43 | "gulp-sass": "^0.7.3",
44 | "gulp-size": "^0.3.0",
45 | "gulp-sourcemaps": "^1.1.1",
46 | "gulp-uglify": "^0.2.1",
47 | "gulp-useref": "^0.4.2",
48 | "jshint-stylish": "^0.2.0",
49 | "main-bower-files": "^1.0.2",
50 | "opn": "^0.1.1",
51 | "v8-profiler": "^5.1.1",
52 | "wiredep": "^1.4.3"
53 | },
54 | "engines": {
55 | "node": "0.10.30"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/npm-debug.log:
--------------------------------------------------------------------------------
1 | 0 info it worked if it ends with ok
2 | 1 verbose cli [ 'c:\\Program Files (x86)\\nodejs\\node.exe',
3 | 1 verbose cli 'c:\\Users\\David\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js',
4 | 1 verbose cli 'version',
5 | 1 verbose cli 'patch' ]
6 | 2 info using npm@2.4.1
7 | 3 info using node@v0.10.36
8 | 4 info git [ 'status', '--porcelain' ]
9 | 5 verbose stack Error: Git working directory not clean.
10 | 5 verbose stack M app/index.html
11 | 5 verbose stack M app/scripts/comments.js
12 | 5 verbose stack M app/scripts/storyPanel.js
13 | 5 verbose stack M app/styles/_storyPanel.scss
14 | 5 verbose stack M app/styles/main.scss
15 | 5 verbose stack M dist/index.html
16 | 5 verbose stack D dist/scripts/main-4c4fe940.js
17 | 5 verbose stack at c:\Users\David\AppData\Roaming\npm\node_modules\npm\lib\version.js:138:37
18 | 5 verbose stack at ChildProcess.exithandler (child_process.js:656:7)
19 | 5 verbose stack at ChildProcess.emit (events.js:98:17)
20 | 5 verbose stack at maybeClose (child_process.js:766:16)
21 | 5 verbose stack at Process.ChildProcess._handle.onexit (child_process.js:833:5)
22 | 6 verbose cwd g:\web\bubblereader
23 | 7 error Windows_NT 6.2.9200
24 | 8 error argv "c:\\Program Files (x86)\\nodejs\\node.exe" "c:\\Users\\David\\AppData\\Roaming\\npm\\node_modules\\npm\\bin\\npm-cli.js" "version" "patch"
25 | 9 error node v0.10.36
26 | 10 error npm v2.4.1
27 | 11 error Git working directory not clean.
28 | 11 error M app/index.html
29 | 11 error M app/scripts/comments.js
30 | 11 error M app/scripts/storyPanel.js
31 | 11 error M app/styles/_storyPanel.scss
32 | 11 error M app/styles/main.scss
33 | 11 error M dist/index.html
34 | 11 error D dist/scripts/main-4c4fe940.js
35 | 12 error If you need help, you may report this error at:
36 | 12 error
37 | 13 verbose exit [ 1, true ]
38 |
--------------------------------------------------------------------------------
/app/scripts/favs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var NB = NB || {};
4 |
5 | NB.Favs = (function() {
6 | var Favs = {};
7 |
8 | var store = [];
9 |
10 | function init() {
11 | //favourite array
12 | if (localStorage.favs) {
13 | var favs = JSON.parse(localStorage.favs);
14 | if (Array.isArray(favs)) {
15 | store = favs;
16 | }
17 | }
18 | }
19 |
20 | Favs.addToFavs = function(story) {
21 | store.push(story);
22 | localStorage.favs = JSON.stringify(store);
23 |
24 | NB.Data.emit('addToFavs', {story: story});
25 | };
26 |
27 | Favs.removeFromFavs = function(story) {
28 | // console.log('Removing story from favs:', story);
29 | var id = story._id;
30 | NB.Data.emit('removeFromFavs', {storyId: story._id});
31 |
32 | store.forEach(function(fav, i) {
33 | if (fav._id === id) {
34 | store.splice(i, 1);
35 | localStorage.favs = JSON.stringify(store);
36 | return;
37 | }
38 | });
39 | };
40 |
41 | Favs.isFav = function(story) {
42 | if (!store.length) { return false; }
43 | var id = story._id;
44 | var hasMatch = false;
45 |
46 | store.forEach(function(fav) {
47 | if (fav._id === id) { hasMatch = true; }
48 | });
49 |
50 | return hasMatch;
51 | };
52 |
53 | //adds/removes from the store, returns true if it's now a fav, false otherwise
54 | Favs.toggleFav = function(koStory) {
55 | var story = koStory.raw;
56 | var isFav = Favs.isFav(story);
57 | if (isFav) {
58 | Favs.removeFromFavs(story);
59 | koStory.isFav(false);
60 | return false;
61 | } else {
62 | Favs.addToFavs(story);
63 | koStory.isFav(true);
64 | return true;
65 | }
66 | };
67 |
68 | Favs.getAll = function() {
69 | return store;
70 | };
71 |
72 |
73 |
74 |
75 | init();
76 | return Favs;
77 | })();
--------------------------------------------------------------------------------
/app/images/gearIcon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | ]>
7 |
22 |
--------------------------------------------------------------------------------
/server/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //devLog will print to console in DEV only.
4 | exports.devLog = function() {
5 | if (!process.env.DEV && !process.env.DEBUG && process.env.LOGGING !== 'DEV') { return; }
6 | var args = arguments;
7 |
8 | function go() {
9 | var result = '';
10 | for (var i = 0; i < args.length; i++) {
11 | result += args[i] + ' ';
12 | }
13 | console.log(result);
14 | }
15 |
16 | process.nextTick(go);
17 | };
18 |
19 | exports.prodLog = function() {
20 | var args = arguments;
21 |
22 | function go() {
23 | var result = '';
24 | for (var i = 0; i < args.length; i++) {
25 | result += args[i] + ' ';
26 | }
27 | console.log(result);
28 | }
29 |
30 | process.nextTick(go);
31 | };
32 |
33 | exports.randomString = function(len) {
34 | function getRandomInt(min, max) {
35 | return Math.floor(Math.random() * (max - min + 1)) + min;
36 | }
37 | var buf = []
38 | , chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
39 | , charlen = chars.length;
40 |
41 | for (var i = 0; i < len; ++i) {
42 | buf.push(chars[getRandomInt(0, charlen - 1)]);
43 | }
44 |
45 | return buf.join('');
46 | };
47 |
48 |
49 | //Proudly butchered from the underscore source
50 | //http://underscorejs.org/docs/underscore.html#section-137
51 |
52 | exports.unescape = function(str) {
53 | var unEscapeMap = {
54 | '&': '&',
55 | '<': '<',
56 | '>': '>',
57 | '"': '"',
58 | ''': "'",
59 | '`': '`'
60 | };
61 |
62 | var escaper = function(match) {
63 | return unEscapeMap[match];
64 | };
65 |
66 | var keysAsString = Object.keys(unEscapeMap).join('|');
67 | var source = '(?:' + keysAsString + ')';
68 | var testRegexp = RegExp(source);
69 | var replaceRegexp = RegExp(source, 'g');
70 |
71 | str = str == null ? '' : '' + str;
72 | return testRegexp.test(str) ? str.replace(replaceRegexp, escaper) : str;
73 |
74 | };
--------------------------------------------------------------------------------
/app/styles/_modal.scss:
--------------------------------------------------------------------------------
1 | /* For fun, trying a different nesting strategy here */
2 | /* Lower classes have short names, but nothing is ever not nested */
3 | .modal-wrapper {
4 | display: none;
5 | opacity: 0;
6 | position: absolute;
7 | top: 0;
8 | width: 100%;
9 | height: 100%;
10 | background: rgba(0, 0, 0, 0.7);
11 | @include shadow;
12 | z-index: 2;
13 |
14 | .panel {
15 | position: relative;
16 | width: 500px;
17 | max-width: 100%;
18 | height: 1050px;
19 | max-height: 90%;
20 | margin: 0 auto;
21 | top: 5%;
22 | background: white;
23 | padding: 20px;
24 | overflow-y: auto;
25 |
26 | .close-x {
27 | $size: 40px;
28 | transition: 300ms;
29 | position: absolute;
30 | width: $size;
31 | height: $size;
32 | top: 0;
33 | right: 0;
34 | font-size: $size;
35 | line-height: $size - 4;
36 | text-align: center;
37 | background: $primary-color;
38 | color: white;
39 | cursor: pointer;
40 | &:hover {
41 | background: darken($primary-color, 10%);
42 | }
43 | }
44 | .footer {
45 | position: absolute;
46 | left: 0;
47 | bottom: 0;
48 | right: 0;
49 | /* background: beige; */
50 | height: 80px;
51 | padding: 20px;
52 | }
53 | .save-btn {
54 | transition: 300ms;
55 | float: right;
56 | /* position: absolute; */
57 | /* bottom: 20px; */
58 | /* right: 20px; */
59 | width: 200px;
60 | background: $belizeHole;
61 | padding: 10px 15px;
62 | color: white;
63 | text-align: center;
64 | cursor: pointer;
65 | &:hover {
66 | background: darken($belizeHole, 10%);
67 | }
68 | }
69 | .body {
70 | position: absolute;
71 | top: 106px;
72 | bottom: 80px;
73 | left: 0;
74 | right: 0;
75 | padding: 20px;
76 | overflow-y: auto;
77 |
78 | .row {
79 | margin: 30px 0 20px 0;
80 |
81 | h2 {
82 | font-size: 18px;
83 | margin: 15px 0 7px 0;
84 | }
85 |
86 | }
87 | }
88 |
89 | }
90 | }
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (process.env.NODETIME_ACCOUNT_KEY) {
4 | require('nodetime').profile({
5 | accountKey: process.env.NODETIME_ACCOUNT_KEY,
6 | appName: 'News Bubbles' // optional
7 | });
8 | }
9 |
10 | var path = require('path')
11 | , mongoose = require('mongoose')
12 | , bodyParser = require('body-parser')
13 | , cookieParser = require('cookie-parser')
14 |
15 | , configVars = require(path.join(__dirname, 'config'))
16 | , hxnCrawler = require(path.join(__dirname, 'hxnCrawler'))
17 | , newHxnCrawler = require(path.join(__dirname, 'new-hxncrawler'))
18 | , rdtCrawler = require(path.join(__dirname, 'rdtCrawler'))
19 | , auth = require(path.join(__dirname, 'auth'))
20 | , utils = require(path.join(__dirname, 'utils'))
21 | // , devLog = utils.devLog
22 | , prodLog = utils.prodLog
23 | , workers = require(path.join(__dirname, 'workers'))
24 | ;
25 |
26 |
27 |
28 | var config;
29 | if (process.env.DEV) {
30 | config = configVars.dev;
31 | } else {
32 | config = configVars.prod;
33 | }
34 | global.config = config;
35 |
36 | // console.log('Running with config:', config);
37 |
38 | //TODO change to createConnections
39 | mongoose.connect(config.db.conn);
40 | var db = mongoose.connection;
41 |
42 | exports.start = function(app) {
43 | prodLog('Server Starting');
44 |
45 | var http = require('http').Server(app);
46 | global.io = require('socket.io')(http); //TODO put io in global?
47 | // var io = require('socket.io')(http);
48 | // global.io = io; //IO is used for global emitting
49 |
50 | app.use(cookieParser());
51 | app.use(bodyParser.json());
52 | app.use(bodyParser.urlencoded({extended: true}));
53 |
54 | auth.setUp(app);
55 |
56 | require(path.join(__dirname, 'routes.js'))(app);
57 |
58 | db.on('open', function() {
59 | // hxnCrawler.startCrawler();
60 |
61 |
62 | newHxnCrawler.start();
63 |
64 |
65 | rdtCrawler.startCrawler();
66 | workers.startCleanupWorker();
67 | // workers.startMemoryStatsReporter();
68 |
69 | http.listen(config.db.port);
70 | });
71 |
72 | db.on('error', function(err) {
73 | prodLog('Database connection error:', err);
74 | });
75 |
76 |
77 | };
78 |
79 |
--------------------------------------------------------------------------------
/server/routes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var path = require('path')
3 | , readabilityApi = require(path.join(__dirname, 'readability'))
4 | , storyController = require(path.join(__dirname, 'controllers', 'story.controller'))
5 | , hxnCrawler = require(path.join(__dirname, 'hxnCrawler'))
6 | , rdtCrawler = require(path.join(__dirname, 'rdtCrawler'))
7 | , devLog = require(path.join(__dirname, 'utils')).devLog
8 | , request = require('request')
9 | , userController = require(path.join(__dirname, 'controllers', 'user.controller'))
10 | ;
11 |
12 | module.exports = function(app) {
13 |
14 | //more routes are in auth.js during dev
15 |
16 | io.on('connection', function(socket) {
17 | socket.on('markAsRead', userController.markAsRead);
18 | socket.on('markAsUnread', userController.markAsUnread);
19 | socket.on('addToFavs', userController.addToFavs);
20 | socket.on('updateSettings', userController.updateSettings);
21 | socket.on('removeFromFavs', userController.removeFromFavs);
22 | });
23 |
24 | app.get('/readability/:url', readabilityApi);
25 |
26 | app.get('/api/:source/:limit/:minScore', storyController.getStories);
27 |
28 | app.get('/crawlers/forceHxnFetch', hxnCrawler.forceFetch);
29 |
30 | app.get('/crawlers/forceRdtFetch/:list/:limit', rdtCrawler.forceFetch);
31 |
32 | // app.get('/api/reddit/info', function(req, res) {
33 | // if (!req.isAuthenticated()) { //TODO: make this middleware to share in all reddit routes (isAuthenticated + req.user.reddit.token)
34 | // return res.json({err: 'not logged in'});
35 | // }
36 | // console.log('using token:', req.user.reddit.token);
37 | // console.log('got query:', req.query);
38 | // var url = 'http://www.reddit.com/by_id/' + req.query.id + '.json';
39 | // var options = {
40 | // url: url,
41 | // json: true,
42 | // headers: {
43 | // 'User-Agent': 'news-bubbles.herokuapp.com/0.3.8 by /u/bubble_boi',
44 | // 'Authorization': 'bearer ' + req.user.reddit.token
45 | // }
46 | // };
47 | // console.log('request to submit a request with object:', options);
48 |
49 | // request.get(options, function(err, req, data) {
50 | // console.log('got response from URL:', url);
51 | // console.log('err:', err);
52 | // console.log('data:', data);
53 | // res.json(data);
54 | // });
55 | // });
56 |
57 | };
--------------------------------------------------------------------------------
/server/workers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var path = require('path')
3 | , models = require(path.join(__dirname, 'models', 'Story.model'))
4 | , Story = models.Story
5 | , utils = require(path.join(__dirname, 'utils'))
6 | , devLog = utils.devLog
7 | , prodLog = utils.prodLog;
8 |
9 |
10 | function startCleanupWorker() {
11 | function cull() {
12 | var now = new Date();
13 | prodLog(' -- Running a cull now -- ', now);
14 |
15 | var oneDayAgo = new Date(now - (1 * 24 * 60 * 60 * 1000));
16 | var twoDaysAgo = new Date(now - (2 * 24 * 60 * 60 * 1000));
17 | var fourDaysAgo = new Date(now - (4 * 24 * 60 * 60 * 1000));
18 | var eightDaysAgo = new Date(now - (8 * 24 * 60 * 60 * 1000));
19 |
20 | Story.remove(
21 | {
22 | $or: [
23 | {
24 | $and: [
25 | {postDate: {$lt: oneDayAgo}},
26 | {score: {$lt: 10}}
27 | ]
28 | },
29 | {
30 | $and: [
31 | {postDate: {$lt: twoDaysAgo}},
32 | {score: {$lt: 100}}
33 | ]
34 | },
35 | {
36 | $and: [
37 | {postDate: {$lt: fourDaysAgo}},
38 | {score: {$lt: 1000}}
39 | ]
40 | },
41 | {
42 | $and: [
43 | {postDate: {$lt: eightDaysAgo}},
44 | {score: {$lt: 10000}}
45 | ]
46 | }
47 | ]
48 | }, function (err, data) {
49 | if (err) {
50 | prodLog('Error culling objects:', err);
51 | } else {
52 | devLog('cull removed', data, 'objects');
53 | }
54 | });
55 | }
56 |
57 | cull();
58 | setInterval(function() {
59 | cull();
60 | }, 1 * 60 * 60 * 1000); //hourly
61 |
62 | }
63 |
64 |
65 | function startMemoryStatsReporter() {
66 |
67 | function printMemStats() {
68 | var usage = process.memoryUsage();
69 | var rss = Math.round(+usage.rss / (1024 * 1024)) + 'mb';
70 | var heapTotal = Math.round(+usage.heapTotal / (1024 * 1024)) + 'mb';
71 | var heapUsed = Math.round(+usage.heapUsed / (1024 * 1024)) + 'mb';
72 | prodLog(' -- Memory usage -- | rss:', rss, ' Heap Total:', heapTotal, ' Heap Used:', heapUsed);
73 | }
74 |
75 | setInterval(function() {
76 | process.nextTick(printMemStats);
77 | }, 30000);
78 |
79 | }
80 |
81 | exports.startCleanupWorker = startCleanupWorker;
82 | exports.startMemoryStatsReporter = startMemoryStatsReporter;
83 |
--------------------------------------------------------------------------------
/app/scripts/layout.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.Layout = (function() {
5 |
6 | var Layout = {}
7 | , chartWrapper = d3.select('#chart-wrapper')
8 | , storyPanel = d3.select('#story-panel')
9 | , storyPanelVisible = false
10 | ;
11 |
12 |
13 | /* ------------------- */
14 | /* -- Story Panel -- */
15 | /* ------------------- */
16 | function setChartAndStoryPanelSize() {
17 | chartWrapper.style('width', NB.splitPos + 'px');
18 | storyPanel.style('left', NB.splitPos + 'px');
19 | }
20 |
21 | function init() {
22 | setChartAndStoryPanelSize();
23 |
24 | chartWrapper.style('display', 'block');
25 | storyPanel.style('display', 'block');
26 | }
27 |
28 | function showStoryPanel() {
29 | if (storyPanelVisible) { return; }
30 | storyPanelVisible = true;
31 |
32 | d3.select('#story-panel-toggle').text('»');
33 |
34 | NB.splitPos = document.body.offsetWidth * 0.618;
35 | setChartAndStoryPanelSize();
36 | NB.Chart.resize('fast');
37 | }
38 | function hideStoryPanel(force) {
39 | if (!force && !storyPanelVisible) { return; }
40 | storyPanelVisible = false;
41 |
42 | d3.select('#story-panel-toggle').text('«');
43 |
44 | NB.splitPos = document.body.offsetWidth - NB.RESIZER_WIDTH;
45 | setChartAndStoryPanelSize();
46 | NB.Chart.resize('fast');
47 | }
48 |
49 |
50 | /* -- EXPORTS -- */
51 |
52 | Layout.render = function() {
53 | setChartAndStoryPanelSize();
54 |
55 | //If the orientation flips, don't loose the panel, just hide it:
56 | if (document.body.offsetWidth - NB.splitPos < 100) {
57 | hideStoryPanel(true);
58 | }
59 | if (!storyPanelVisible && (NB.splitPos + NB.RESIZER_WIDTH !== document.body.offsetWidth)) {
60 | hideStoryPanel(true);
61 | }
62 | };
63 |
64 | Layout.moveSplitPos = function() {
65 | if (!storyPanelVisible) { //the divider is dragged out from the edge
66 | storyPanelVisible = true;
67 | d3.select('#story-panel-toggle').text('»');
68 | }
69 |
70 | setChartAndStoryPanelSize();
71 | };
72 |
73 | Layout.showStoryPanel = function() {
74 | showStoryPanel();
75 | };
76 |
77 | Layout.hideStoryPanel = function() {
78 | hideStoryPanel();
79 | };
80 | Layout.toggleStoryPanel = function() {
81 | if (storyPanelVisible) {
82 | hideStoryPanel();
83 | } else {
84 | showStoryPanel();
85 | }
86 |
87 | };
88 |
89 |
90 |
91 |
92 | Layout.init = function() {
93 | init();
94 | };
95 |
96 |
97 | return Layout;
98 | })();
--------------------------------------------------------------------------------
/app/styles/_layout.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | @include shadow();
3 | height: $header-height;
4 | background: $header-color;
5 | position: relative;
6 | z-index: 2;
7 | overflow: hidden;
8 | &-title {
9 | float: left;
10 | padding: 10px;
11 | margin: 0;
12 | font-size: 22px;
13 | color: white;
14 | font-weight: normal;
15 | letter-spacing: 1px;
16 | @media (max-width: 760px) {
17 | display: none;
18 | }
19 |
20 | .by-line {
21 | font-family: monospace;
22 | font-size: 12px;
23 | color: black;
24 | @media (max-width: 830px) {
25 | display: none;
26 | }
27 | }
28 | }
29 | .header-float {
30 | float: right;
31 | height: 100%;
32 | }
33 | .user-items {
34 | height: 100%;
35 | padding: 0 0 0 20px;
36 | color: white;
37 | .user-item {
38 | //TODO not sure how to stop FOUC
39 | /* display: none; //Overridden by knockout */
40 | color: white;
41 | /* text-decoration: none; */
42 | &.name {
43 | display: inline-block;
44 | position: relative;
45 | top: -5px;
46 | padding-right: 10px;
47 | @media (max-width: 600px) {
48 | display: none;
49 | }
50 | }
51 | &.sign {
52 | padding: 13px 14px;
53 | }
54 | &.icon {
55 | padding: 9px;
56 | }
57 | }
58 | }
59 | .news-sources {
60 | cursor: pointer;
61 | &-source {
62 | transition: 200ms;
63 | @include no-style-button;
64 | color: white;
65 | font-size: 17px;
66 | height: 100%;
67 | padding: 0 12px;
68 | margin-left: -3px; //narrow the inline-block gap
69 | background: $primary-color;
70 | body.no-touch &:hover {
71 | background: white;
72 | color: $primary-color;
73 | }
74 | &.active {
75 | background: white;
76 | color: $primary-color;
77 | }
78 | }
79 | }
80 | &-icons-wrapper {
81 | float: right;
82 | width: 60px;
83 | height: 40px;
84 | padding: 9px;
85 | text-align: center;
86 | cursor: pointer;
87 | color: white;
88 | }
89 | &-btn { //TODO what is this?
90 | float: right;
91 | font-size: 16px;
92 | height: 100%;
93 | padding: 12px 15px;
94 | background-color: white;
95 | color: $primary-color;
96 | cursor: pointer;
97 | @media (max-width: 500px) {
98 | padding: 12px 5px;
99 | font-size: 14px;
100 | }
101 | @media (max-width: 350px) {
102 | padding: 14px 8px;
103 | font-size: 12px;
104 | }
105 | }
106 | }
107 | .nb-content-wrapper {
108 | position: absolute;
109 | top: $header-height;
110 | left: 0;
111 | bottom: 0;
112 | right: 0;
113 | overflow: hidden;
114 | }
115 |
--------------------------------------------------------------------------------
/app/styles/_tooltip.scss:
--------------------------------------------------------------------------------
1 | #tooltip {
2 | @include shadow();
3 | visibility: hidden;
4 | position: fixed;
5 | left: 100px;
6 | top: 100px;
7 | background: $primary-color;
8 | color: white;
9 | text-align: center;
10 | padding: 10px;
11 | max-width: 200px;
12 | word-wrap: break-word;
13 | z-index: 9;
14 | }
15 |
16 | .maxi-tooltip {
17 | $width: 540px;
18 | $height: 150px;
19 | $dark: #2c3e50;
20 | $light: #7f8c8d;
21 | $radius: 40px;
22 |
23 | /* display: none; */
24 | position: fixed;
25 | left: 20px;
26 | bottom: 20px;
27 | width: $width;
28 | height: $height;
29 | background: black;
30 | color: white;
31 | /* border-radius: $radius; */
32 | @include shadow();
33 | z-index: 2;
34 |
35 | a {
36 | color: white;
37 | text-decoration: none;
38 | &:hover {
39 | text-decoration: underline;
40 | }
41 | }
42 |
43 | .table {
44 | display: table;
45 | .row {
46 | display: table-row;
47 | }
48 | .cell {
49 | display: table-cell;
50 | vertical-align: middle;
51 | }
52 | }
53 | .left {
54 | float: left;
55 | width: 33%;
56 | height: 100%;
57 | background: darken($primary-color, 15%);
58 | /* border-radius: $radius 0 0 $radius; */
59 |
60 | .btn {
61 | transition: 300ms;
62 | width: 100%;
63 | text-align: center;
64 | height: $height / 3;
65 | cursor: pointer;
66 | &:hover {
67 | background: darken($primary-color, 20%);
68 | text-decoration: none;
69 | }
70 | &.left-top {
71 | /* border-radius: $radius 0 0 0; */
72 | }
73 | &.left-middle {
74 | box-sizing: content-box; //border box is ignored in chrome and god knows where else
75 | height: $height / 3 - 2;
76 | border-top: 1px solid darken($primary-color, 10%);
77 | border-bottom: 1px solid darken($primary-color, 10%);
78 | }
79 | &.left-bottom {
80 | /* border-radius: 0 0 0 $radius; */
81 | }
82 | }
83 | }
84 | .right {
85 | float: left;
86 | width: 67%;
87 | height: 100%;
88 | background: $primary-color;
89 | border-left: 1px solid white;
90 | /* border-radius: 0 $radius $radius 0; */
91 |
92 | &-top {
93 | height: $height / 2;
94 | width: 100%;
95 | padding: 0 10px;
96 | border-bottom: 1px solid lighten($primary-color, 10%);
97 | overflow-y: hidden;
98 |
99 | h1 {
100 | color: white;
101 | height: 100%;
102 | width: 100%;
103 | font-size: 25px;
104 | font-weight: 300;
105 | margin: 0;
106 | }
107 | }
108 | &-bottom {
109 | height: $height * 0.5;
110 | width: 100%;
111 | padding: 12px 10px 0 10px;
112 | font-size: 13px;
113 | p {
114 | margin: 0 7px 0 0;
115 | white-space: nowrap;
116 | overflow: hidden;
117 | }
118 | &-left {
119 | float: left;
120 | width: 62%;
121 | height: 100%;
122 | }
123 | &-right {
124 | float: left;
125 | width: 38%;
126 | height: 100%;
127 | text-align: right;
128 | }
129 | }
130 | }
131 | }
--------------------------------------------------------------------------------
/app/scripts/actions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.Actions = (function() {
5 | var Actions = {};
6 | //TODO get reference to both tooltips in here?
7 | var maxiTooltipShowing = false;
8 |
9 | function init() {
10 |
11 |
12 | }
13 |
14 |
15 | function showTooltip(options) { //TODO 'd' is stupid
16 | var setting = NB.Settings.getSetting('clickAction')
17 | , d = options.story
18 | , el = options.domEl
19 | , w = options.chartWidth
20 | ;
21 |
22 |
23 | if (setting === 'storyPanel') {
24 | NB.Data.markAsRead(d._id);
25 | el.classed('read', true);
26 | NB.Layout.showStoryPanel();
27 | NB.StoryPanel.render(d);
28 | }
29 |
30 | if (setting === 'storyTooltip') { //TODO move this out to maxiTooltip module (pass el and d)
31 | var maxiTooltip = d3.select('#story-tooltip'); //TODO move these up to top of NB.Actions
32 | var tooltipWidth = parseInt(maxiTooltip.style('width'));
33 | var tooltipHeight = parseInt(maxiTooltip.style('height'));
34 |
35 | var thisDims = el.node().getBoundingClientRect();
36 |
37 | // var r = z(d.commentCount);
38 | var left = thisDims.left + (thisDims.width / 2) - (tooltipWidth / 2);
39 | var maxLeft = w - tooltipWidth - 20;
40 | left = Math.min(left, maxLeft);
41 | left = Math.max(left, 0);
42 |
43 | var top = thisDims.top - tooltipHeight;
44 | if (top < 50) {
45 | top = thisDims.bottom;
46 | }
47 |
48 | NB.StoryModel.setCurrentStory(d); //TODO should this make visible? E.g. control vis in model?
49 |
50 | var readUnreadLink = d3.select('#tooltip-mark-as-read');
51 | if (el.classed('read')) {
52 | readUnreadLink.text('Mark as unread');
53 | } else {
54 | readUnreadLink.text('Mark as read');
55 | }
56 |
57 |
58 | var duration = maxiTooltipShowing ? 200 : 0;
59 | maxiTooltip
60 | .style('display', 'block')
61 | .transition()
62 | .duration(duration)
63 | .style('left', left + 'px')
64 | .style('top', top + 'px');
65 |
66 | maxiTooltipShowing = true; //will block little tooltip from showing
67 |
68 | d3.event.stopPropagation(); //TODO I do not know the diff between this and immediate. Immediate stops other events on this el?
69 |
70 | $(document).on('click.tooltip', function() { //TODO try .one, still not working?
71 | maxiTooltip.style('display', 'none');
72 | $(document).off('click.tooltip');
73 |
74 | window.setTimeout(function() {
75 | maxiTooltipShowing = false; //wait a bit before allowing the little tooltip to show
76 | }, 300);
77 | });
78 | readUnreadLink.on('click', function() { //D3 will remove any existing listener
79 | toggleRead(el, d);
80 | });
81 | d3.select('#tooltip-open-reading-pane').on('click', function() { //D3 will remove any existing listener
82 | NB.Data.markAsRead(d.id);
83 | el.classed('read', true);
84 | NB.Layout.showStoryPanel();
85 | NB.StoryPanel.render(d);
86 | });
87 |
88 | }
89 |
90 | }
91 |
92 | /* --------------- */
93 | /* -- Exports -- */
94 | /* --------------- */
95 |
96 | Actions.showTooltip = showTooltip;
97 |
98 | init();
99 | return Actions;
100 | })();
--------------------------------------------------------------------------------
/app/scripts/auth.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.Auth = (function() {
5 | var Auth = {}
6 | , rawUser = {}
7 | , authModal = d3.select('#auth-modal')
8 | ;
9 |
10 | // Remove the ugly Facebook appended hash
11 | //
12 | // source for this code: https://github.com/jaredhanson/passport-facebook/issues/12#issuecomment-5913711
13 | function removeFacebookAppendedHash() {
14 | if (!window.location.hash || window.location.hash !== '#_=_') {
15 | return;
16 | } else if (window.history && window.history.replaceState) {
17 | return window.history.replaceState('', document.title, window.location.pathname);
18 | } else {
19 | window.location.hash = '';
20 | }
21 |
22 |
23 | }
24 |
25 | function close() {
26 | authModal
27 | .transition().duration(500)
28 | .style('opacity', 0)
29 | .transition()
30 | .style('display', 'none');
31 | }
32 |
33 | function save() {
34 | close();
35 | }
36 |
37 | function open() {
38 | authModal
39 | .style('display', 'block')
40 | .transition().duration(500)
41 | .style('opacity', 1);
42 | }
43 |
44 | var userModel = {
45 | _id: '',
46 | name: {
47 | first: ko.observable(''),
48 | last: ko.observable(''),
49 | display: ko.observable('')
50 | },
51 | displayName: ko.observable(''),
52 | signedIn: ko.observable(false),
53 | headerText: ko.observable('Sign in'),
54 | open: open,
55 | close: close,
56 | save: save
57 | };
58 |
59 |
60 | function init() {
61 | // ko.applyBindings(userModel, document.getElementById('user-items'));
62 | // ko.applyBindings(userModel, document.getElementById('auth-modal'));
63 | }
64 |
65 |
66 | function setUser(user) {
67 | if (user.reddit) {
68 | $('body').addClass('user-rdt');
69 | // $('#story-panel-header').addClass('show-vote-btns');
70 | } else {
71 | $('body').removeClass('user-rdt');
72 | // $('#story-panel-header').removeClass('show-vote-btns');
73 | }
74 | rawUser = user;
75 | var displayName = user.displayName || user.name.display;
76 | if (user) {
77 | userModel._id = user._id;
78 | userModel.displayName(displayName);
79 | userModel.signedIn(true);
80 | userModel.headerText(displayName);
81 | removeFacebookAppendedHash(); //TODO test for FB?
82 | } else {
83 | userModel._id = null;
84 | userModel.displayName(null);
85 | userModel.signedIn(false);
86 | userModel.headerText('Sign in'); //TODO not used when no signed in
87 | }
88 | }
89 |
90 | function getUser() {
91 | if (userModel.signedIn()) {
92 | return userModel;
93 | } else {
94 | return null;
95 | }
96 |
97 | }
98 | function getRawUser() {
99 | return rawUser;
100 | }
101 |
102 | function signOut() {
103 | console.log('OK, will sign out (ha ha, but I am not really!');
104 | }
105 |
106 |
107 | /* --------------- */
108 | /* -- Exports -- */
109 | /* --------------- */
110 |
111 | Auth.setUser = setUser;
112 | Auth.getUser = getUser;
113 | Auth.getRawUser = getRawUser;
114 | Auth.signOut = signOut;
115 | Auth.userModel = userModel;
116 |
117 | init();
118 | return Auth;
119 | })();
--------------------------------------------------------------------------------
/app/scripts/storyModel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.StoryModel = (function() {
5 | var StoryModel = {};
6 |
7 | /* -- Story methods -- */
8 | function rdtVote(upOrDown) {
9 | var data = {
10 | upOrDown: upOrDown,
11 | id: StoryModel.storyModel.raw._id,
12 | sourceId: StoryModel.storyModel.raw.sourceId
13 | };
14 |
15 | StoryModel.storyModel.userVote(upOrDown);
16 |
17 | $.post('/api/reddit/vote', data, function(res) {
18 | if (res.err) {
19 | console.log('Error saving a vote:', res);
20 | }
21 | });
22 |
23 | }
24 |
25 | function init() {
26 | StoryModel.storyModel = {
27 | raw: {},
28 | name: ko.observable(),
29 | shortName: ko.observable(),
30 | url: ko.observable(),
31 | sourceUrl: ko.observable(),
32 | authorUrl: ko.observable(),
33 | category: ko.observable(),
34 | color: ko.observable(),
35 | author: ko.observable(),
36 | commentCount: ko.observable(),
37 | score: ko.observable(),
38 | timeString: ko.observable(),
39 | dateString: ko.observable(),
40 | content: ko.observable(''),
41 | isFav: ko.observable(false),
42 | userVote: ko.observable(''),
43 | upVote: function() {
44 | if (StoryModel.storyModel.userVote() !== 'up') { //'this' refers to NB.App
45 | rdtVote('up');
46 | } else {
47 | rdtVote('');
48 | }
49 | },
50 | downVote: function() {
51 | if (StoryModel.storyModel.userVote() !== 'down') { //'this' refers to NB.App
52 | rdtVote('down');
53 | } else {
54 | rdtVote('');
55 | }
56 | }
57 | };
58 | }
59 |
60 | function setCurrentStory(story) {
61 | var dateFormatter = d3.time.format('%a, %-e %b %Y')
62 | , timeFormatter = d3.time.format('%-I:%M%p')
63 | , domain
64 | , shortName = story.name
65 | , isFav = NB.Favs.isFav(story)
66 | , color = NB.Settings.getColor(story.source, story.category)
67 | ;
68 |
69 |
70 | if (name.length > 50) { //TODO push to database?
71 | shortName = name.substr(0, 47).trim() + '...';
72 | }
73 |
74 | StoryModel.storyModel.raw = story;
75 |
76 | StoryModel.storyModel
77 | .name(story.name)
78 | .shortName(shortName)
79 | .url(story.url)
80 | .sourceUrl(story.sourceUrl)
81 | .authorUrl(story.authorUrl)
82 | .category(story.category)
83 | .color(color)
84 | .author(story.author)
85 | .commentCount(story.commentCount)
86 | .score(Math.round(story.score))
87 | .timeString(timeFormatter(story.postDate))
88 | .dateString(dateFormatter(story.postDate))
89 | .content(story.content)
90 | .isFav(isFav)
91 | .userVote(story.vote);
92 | }
93 |
94 | function clear() {
95 | StoryModel.storyModel
96 | .name('')
97 | .url('')
98 | .sourceUrl('')
99 | .authorUrl('')
100 | .category('')
101 | .color('')
102 | .author('')
103 | .commentCount('')
104 | .score('')
105 | .timeString('')
106 | .dateString('')
107 | .content('')
108 | .isFav('')
109 | .userVote('');
110 | }
111 |
112 |
113 | /* --------------- */
114 | /* -- Exports -- */
115 | /* --------------- */
116 |
117 | StoryModel.setCurrentStory = setCurrentStory;
118 | StoryModel.clear = clear;
119 |
120 | init();
121 | return StoryModel;
122 | })();
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var path = require('path');
5 |
6 | // load plugins
7 | var $ = require('gulp-load-plugins')();
8 |
9 | gulp.task('sass', function () {
10 | return gulp.src('app/styles/main.scss')
11 | .pipe($.sass())
12 | // .pipe($.autoprefixer('last 1 version'))
13 | .pipe(gulp.dest('.tmp/styles'))
14 | .pipe($.size());
15 |
16 | });
17 |
18 | gulp.task('styles', ['sass'], function() {
19 | return gulp.src('.tmp/styles/main.css')
20 | .pipe($.sourcemaps.init({
21 | loadMaps: true,
22 | includeContent: false,
23 | sourceRoot: '../cats'
24 | }))
25 | .pipe($.sourcemaps.write('.'))
26 | .pipe(gulp.dest('.tmp/styles'));
27 | });
28 |
29 | gulp.task('scripts', function () {
30 | return gulp.src('app/scripts/**/*.js')
31 | .pipe($.jshint())
32 | .pipe($.jshint.reporter(require('jshint-stylish')))
33 | .pipe($.size());
34 | });
35 |
36 | gulp.task('html', ['styles', 'scripts'], function () {
37 | var jsFilter = $.filter('**/*.js');
38 | var cssFilter = $.filter('**/*.css');
39 |
40 | return gulp.src('app/index.html')
41 | .pipe($.useref.assets({searchPath: '{.tmp,app}'}))
42 | .pipe(jsFilter)
43 | .pipe($.uglify())
44 | .pipe(jsFilter.restore())
45 | .pipe(cssFilter)
46 | .pipe($.csso())
47 | .pipe($.minifyCss())
48 | .pipe(cssFilter.restore())
49 | .pipe($.rev())
50 | .pipe($.useref.restore())
51 | .pipe($.useref())
52 | .pipe($.revReplace())
53 | .pipe(gulp.dest('dist'))
54 | .pipe($.size());
55 | });
56 |
57 | gulp.task('extras', function () {
58 | return gulp.src(['app/*.*', '!app/index.html'], { dot: true })
59 | .pipe(gulp.dest('dist'));
60 | });
61 |
62 |
63 | gulp.task('clean', function () {
64 | return gulp.src(['.tmp', 'dist'], { read: false })
65 | .pipe($.clean());
66 | });
67 |
68 |
69 | gulp.task('express', ['mongod'], function () {
70 | var express = require('express')
71 | , app = express();
72 |
73 | app.use(require('connect-livereload')({ port: 35729 }));
74 |
75 | app.use(require('compression')());
76 | app.use(express.static('app'));
77 | app.use(express.static('.tmp'));
78 | process.env.DEV = true;
79 | process.env.DEBUG = true;
80 |
81 | var server = require(path.join(__dirname, 'server', 'server.js'));
82 | server.start(app);
83 |
84 | });
85 |
86 |
87 | gulp.task('mongod', function() {
88 | //From here: http://stackoverflow.com/questions/18334181/spawn-on-node-js-windows-server-2012
89 | var spawn = require('child_process').spawn;
90 | spawn(process.env.comspec, ['/c', 'start mongod',]);
91 | });
92 |
93 |
94 | // inject bower components
95 | gulp.task('wiredep', function () {
96 | var wiredep = require('wiredep').stream;
97 |
98 | gulp.src('app/styles/*.scss')
99 | .pipe(wiredep({
100 | directory: 'app/bower_components'
101 | }))
102 | .pipe(gulp.dest('app/styles'));
103 |
104 | gulp.src('app/*.html')
105 | .pipe(wiredep({
106 | directory: 'app/bower_components'
107 | }))
108 | .pipe(gulp.dest('app'));
109 | });
110 |
111 |
112 | gulp.task('watch', ['express'], function () {
113 | var server = $.livereload();
114 |
115 | // watch for changes
116 |
117 | gulp.watch([
118 | 'app/*.html',
119 | '.tmp/styles/**/*.css',
120 | 'app/scripts/**/*.js',
121 | 'app/images/**/*'
122 | ]).on('change', function (file) {
123 | server.changed(file.path);
124 | });
125 |
126 | gulp.watch('app/styles/**/*.scss', ['styles']);
127 | gulp.watch('app/scripts/**/*.js', ['scripts']);
128 | gulp.watch('app/images/**/*', ['images']);
129 | gulp.watch('bower.json', ['wiredep']);
130 | });
131 |
132 | gulp.task('build', ['html', 'extras']);
133 | // gulp.task('build', ['html', 'images', 'fonts', 'extras']);
134 |
135 | gulp.task('default', ['clean'], function () {
136 | gulp.start('build');
137 | });
138 |
--------------------------------------------------------------------------------
/app/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found :(
6 |
141 |
142 |
143 |
144 |
Not found :(
145 |
Sorry, but the page you were trying to view does not exist.
146 |
It looks like this was the result of either:
147 |
148 | - a mistyped address
149 | - an out-of-date link
150 |
151 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/dist/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found :(
6 |
141 |
142 |
143 |
144 |
Not found :(
145 |
Sorry, but the page you were trying to view does not exist.
146 |
It looks like this was the result of either:
147 |
148 | - a mistyped address
149 | - an out-of-date link
150 |
151 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/app/styles/_storyPanel.scss:
--------------------------------------------------------------------------------
1 | .story-panel {
2 | transition: $move-dur; //turned off while dragging
3 | position: absolute;
4 | display: none; //overridden on startup
5 | left: 100%; //overridden on startup
6 | height: 100%;
7 | @include shadow();
8 | right: 0px;
9 | padding-left: 28px;
10 | background-color: white;
11 | z-index: 1;
12 |
13 | .story-panel-resizer {
14 | position: absolute;
15 | left: 0;
16 | top: 0;
17 | height: 100%;
18 | width: 28px;
19 | /* background-color: $primary-color; */
20 | background-color: white;
21 | overflow-y: hidden;
22 | cursor: ew-resize;
23 | &.active .story-panel-resizer-dots {
24 | border: 2px dotted lighten($primary-color, 20%);
25 | }
26 | &-dots {
27 | float: left;
28 | margin-top: -10%;
29 | height: 120%;
30 | width: 8px;
31 | margin-left: 4px;
32 | border: 2px dotted lighten($primary-color, 30%);
33 | }
34 | .story-panel-toggle {
35 | position: absolute;
36 | top: 50%;
37 | left: 0px;
38 | margin-top: -50px;
39 | width: 100%;
40 | height: 100px;
41 | font-size: 40px;
42 | line-height: 95px;
43 | text-align: center;
44 | color: $primary-color;
45 | cursor: pointer;
46 | background-color: white;
47 | }
48 | }
49 |
50 | .story-wrapper {
51 | height: 100%;
52 | min-width: 200px;
53 | overflow-y: scroll;
54 | -webkit-overflow-scrolling: touch;
55 | padding: 10px 20px 50px 10px;
56 | background-color: white;
57 | z-index: 0;
58 | @media (max-width: 500px) {
59 | padding: 7px;
60 | }
61 |
62 | .story-title {
63 | .active .line-art {
64 | fill-opacity: 1;
65 | }
66 |
67 | .vote-button-wrapper {
68 | display: none;
69 | position: absolute;
70 | padding-left: 3px;
71 | left: 0;
72 | width: 30px;
73 |
74 | .vote-button {
75 | transition: 300ms;
76 | color: $primary-color;
77 | opacity: 0.3;
78 |
79 | &:hover {
80 | opacity: 1;
81 | }
82 |
83 | }
84 | &.up .vote-button.up {
85 | opacity: 1;
86 | }
87 | &.down .vote-button.down {
88 | opacity: 1;
89 | }
90 | body.user-rdt.rdt & {
91 | display: block;
92 | }
93 | }
94 | .meta-data {
95 | position: absolute;
96 | left: 0;
97 | top: 2px;
98 |
99 | body.user-rdt.rdt & {
100 | left: 50px;
101 | }
102 | }
103 | h1 {
104 | font-size: 24px;
105 | font-weight: 300;
106 | color: #222;
107 | line-height: 26px;
108 | a {
109 | text-decoration: none;
110 | font-size: inherit;
111 | }
112 | }
113 | .sub-title {
114 | position: relative;
115 | height: 65px;
116 |
117 | .category-dot {
118 | display: inline-block;
119 | position: relative;
120 | top: -3px;
121 | width: 13px;
122 | height: 13px;
123 | border-radius: 50%;
124 | margin-right: 7px;
125 | vertical-align: bottom;
126 | }
127 | a {
128 | text-decoration: none;
129 | }
130 | p {
131 | margin-bottom: 6px;
132 | margin-top: 0;
133 | }
134 | font-size: 12px;
135 | line-height: 2;
136 | margin-bottom: 10px;
137 | color: #777;
138 | }
139 | }
140 | p {
141 | line-height: 1.5;
142 | margin-bottom: 30px;
143 | }
144 | img {
145 | display: block;
146 | max-width: 100%;
147 | height: auto;
148 | }
149 | figcaption {
150 | margin-bottom: 20px;
151 | font-style: italic;
152 | }
153 | .comment-separator {
154 | border-bottom: 1px solid $primary-color;
155 | margin-top: 40px;
156 | padding: 9px 0;
157 | text-align: center;
158 | }
159 |
160 | .comment-list {
161 | list-style-type: none;
162 | margin: 0;
163 | padding: 0 0 0 10px;
164 |
165 | &-title {
166 | font-style: italic;
167 | margin: 15px 0;
168 | }
169 | &.level-1 {
170 | padding: 0;
171 | }
172 | &-item {
173 | border-left: 1px dotted #ccc;
174 | padding-left: 5px;
175 | margin-top: 25px;
176 |
177 | blockquote {
178 | border-left: 2px solid $primary-color;
179 | margin: 10px 0 10px 10px;
180 | padding-left: 10px;
181 | }
182 |
183 | &-text {
184 | margin: 0;
185 | padding: 0;
186 | &.meta {
187 | padding: 3px 0 0 10px;
188 | font-size: 13px;
189 | color: #999;
190 | a.reply {
191 | color: #999;
192 | &:hover {
193 | color: $primary-color;
194 | }
195 | }
196 | }
197 | &.body p {
198 | margin: 0;
199 | }
200 | }
201 | }
202 | }
203 | }
204 | }
205 |
206 | .show-story-panel {
207 | .story-panel {
208 | margin-right: 0px;
209 | &-grabber-inner {
210 | transform: scaleX(1);
211 | }
212 | &-closer:before {
213 | content: '»';
214 | }
215 | }
216 | #more-btn {
217 | right: 425px;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/server/controllers/user.controller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path')
4 | , User = require(path.join(__dirname, '..', 'models', 'User.model'))
5 | , utils = require(path.join(__dirname, '..', 'utils'))
6 | , devLog = utils.devLog
7 | , request = require('request')
8 | ;
9 |
10 |
11 | //add this id to the read list for the user
12 | function markAsRead(data) {
13 | // devLog('will add to read list:', data.userId, 'and', data.storyId);
14 | var userId = data.userId
15 | , storyId = data.storyId
16 | ;
17 | User.findById(userId, function(err, user) {
18 | if (err) { return; } //TODO feed back to client
19 | if (!user) { return; } //perhaps user was deleted in another session? TODO hande better
20 |
21 | var foundMatch = false
22 | , i
23 | ;
24 |
25 | if (user.stories) { //older stories won't have this. Can go if I do a DB wipe
26 | for (i = 0; i < user.stories.length; i++) {
27 | if (user.stories[i].storyId === storyId) {
28 | user.stories[i].read = true;
29 | foundMatch = true;
30 | break;
31 | }
32 | }
33 | }
34 |
35 | if (!foundMatch) {
36 | user.stories.push({
37 | storyId: storyId,
38 | read: true
39 | });
40 | }
41 | user.save();
42 |
43 |
44 | });
45 | // User.findById(userId, function(err, user) {
46 | // if (err) { return; } //TODO feed back to client
47 | // if (!user) { return; } //perhaps user was deleted in another session? TODO hande better
48 |
49 | // if (user.readList.indexOf(storyId) === -1) {
50 | // // console.log('Adding', storyId, 'to the list of read things for user', userId);
51 | // user.readList.push(storyId);
52 | // user.save();
53 | // }
54 |
55 | // });
56 | }
57 | //add this id to the read list for the user
58 | function markAsUnread(data) {
59 | // devLog('will remove from read list:', data.userId, 'and', data.storyId);
60 | var userId = data.userId
61 | , storyId = data.storyId
62 | ;
63 | User.findById(userId, function(err, user) {
64 | if (err) { return; } //TODO feed back to client
65 | if (!user) { return; } //perhaps user was deleted in another session? TODO hande better
66 | var i;
67 |
68 | if (user.stories) { //older stories won't have this. Can go if I do a DB wipe
69 | for (i = 0; i < user.stories.length; i++) {
70 | if (user.stories[i].storyId === storyId) {
71 | user.stories[i].read = false;
72 | break;
73 | }
74 | }
75 | }
76 | user.save();
77 |
78 | // var pos = user.readList.indexOf(storyId);
79 | // if (pos > -1) {
80 | // // devLog('Marking this story as not read:', user.readList[pos]);
81 | // user.readList.splice(pos, 1);
82 | // user.save();
83 | // } else {
84 | // devLog('No story with id', storyId, 'is in the read list. That is odd.');
85 | // }
86 |
87 | });
88 | }
89 |
90 | function addToFavs(data) {
91 | //TODO for now I'm adding the entire story to the user object.
92 | //Eventually just store the ID, then generate the fav list when a user navigates to fav tab.
93 | // devLog('will add to favs:', data.userId, 'and', data.story.name);
94 | var userId = data.userId
95 | , story = data.story
96 | ;
97 | User.findById(userId, function(err, user) {
98 | if (err) { return; } //TODO feed back to client
99 | if (!user) { return; } //perhaps user was deleted in another session? TODO hande better
100 | var storyExists = false;
101 | user.favs.forEach(function(fav) {
102 | if (fav.id === story.id) { storyExists = true; }
103 | });
104 | if (storyExists) {
105 | return;
106 | } else {
107 | user.favs.push(story);
108 | user.save();
109 | }
110 |
111 | });
112 | }
113 |
114 | function updateSettings(data) {
115 | var userId = data.userId
116 | , settings = data.settings
117 | ;
118 | User.findById(userId, function(err, user) {
119 | if (err) { return; } //TODO feed back to client
120 | if (!user) { return; } //perhaps user was deleted in another session? TODO hande better
121 |
122 | //TODO the settings sent from the client that aren't the schema will be ignored
123 | //but still, I should be less brutal about what I save here.
124 | user.settings = settings;
125 | user.save();
126 | });
127 |
128 | }
129 |
130 | function removeFromFavs(data) {
131 | var userId = data.userId
132 | , storyId = data.storyId
133 | ;
134 | User.findById(userId, function(err, user) {
135 | if (err) { return; } //TODO feed back to client
136 | if (!user) { return; } //perhaps user was deleted in another session? TODO hande better
137 | user.favs.forEach(function(fav, i) {
138 | if (fav.id === storyId) {
139 | user.favs.splice(i, 1);
140 | user.save();
141 | return;
142 | }
143 | });
144 | });
145 |
146 | }
147 |
148 | function updateToken(userId, token, done) {
149 | console.log('updateToken(', userId, token, ')');
150 | User.findById(userId, function(err, userDoc) {
151 | console.log('saving for user:', userDoc);
152 | userDoc.reddit.token = token;
153 | userDoc.save(function(err) {
154 | done(err);
155 | });
156 | });
157 | }
158 |
159 | function rdtVote(req, res) {
160 | }
161 |
162 | exports.markAsRead = markAsRead;
163 | exports.markAsUnread = markAsUnread;
164 | exports.addToFavs = addToFavs;
165 | exports.updateSettings = updateSettings;
166 | exports.removeFromFavs = removeFromFavs;
167 | exports.rdtVote = rdtVote;
168 | exports.updateToken = updateToken;
169 |
170 |
--------------------------------------------------------------------------------
/app/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | About
6 |
15 |
16 |
17 | Back to the bubbles
18 | Release Notes
19 |
20 | Version 0.4.5
21 | 11 October 2014
22 | New
23 |
24 | - Links to YouTube videos in comments will be replaced with in-line YouTube players.
25 | - Hacker News stories now real-time (using the new FireBase API).
26 | - Links to reply to comments for Hacker News and Reddit.
27 | - Pretty new icons.
28 |
29 | Fixed
30 |
31 | - Hacker News bubbles with zero comments are not showing.
32 | - Deleted Reddit comments are showing.
33 | - Vote buttons show when voting isn't possible.
34 |
35 |
36 |
37 |
38 | Version 0.4.0
39 | 7 October 2014
40 | New
41 |
42 | - Sign in with Reddit to vote (comments and comment voting coming soon).
43 |
44 | Fixed
45 |
46 | - Oh heaps of stuff.
47 |
48 |
49 |
50 |
51 | Version 0.3.7
52 | 2 October 2014
53 | New
54 |
55 | - Sign in with Facebook to sync across devices: settings, already read items and favourites.
56 | - Pictures in comments show in-line (only direct links to images for now, not imgur pages, etc.)
57 |
58 | Fixed
59 |
60 | - Database fails to store new stories.
61 | - Tooltip covers circle in IE.
62 | - Tooltip shows on touch devices.
63 | - Header items don't fit on small devices.
64 | - If comments are slow to load, they can take focus of the story panel
65 | - "\n" showing in comments.
66 | - [Deleted] reddit comments showing
67 |
68 |
69 |
70 |
71 | Version 0.3.6
72 | 24 September 2014
73 | New
74 |
75 | - The name. Bye bye 'News bubbles', hello 'Bubble Reader'.
76 |
77 | Fixed
78 |
79 | - Database reached the max allowed size because I'm storing a whole lot of crap that I don't need to be.
80 | - Subreddit links in story comments (e.g. /r/dogs_and_cats) don't work.
81 | - Links in comments open in the same page (yes, I'm calling that a bug).
82 | - Page can take several seconds to load even on fast connection.
83 | - Memory leak on server causes intermittent shutdown.
84 |
85 |
86 |
87 |
88 | Version 0.3.5
89 | 19 September 2014
90 | New
91 |
92 | - Favorites! Star an item in the story panel and it will appear on the favourites page.
93 |
94 | Fixed
95 |
96 | - Right click doesn't send circle to back.
97 | - 'null' shows below images from imgur gallery if there is no title.
98 |
99 |
100 |
101 |
102 | Version 0.3.3
103 | 16 September 2014
104 | New
105 |
106 | - Comments for Reddit stories now show below image/article in the story panel.
107 | - imgur.com galleries display with inline captions.
108 |
109 | Fixed
110 |
113 |
114 |
115 |
116 | Version 0.3.2
117 | 15 September 2014
118 | New
119 |
120 | - Moved hosting from MongoHQ (Compose) to MongoLab.
121 | - Dynamic y-axis exponent keeps bubbles more evenly distributed.
122 |
123 | Fixed
124 |
125 | - Crawler error when forcing a crawl.
126 |
127 |
128 |
129 |
130 | Version 0.3.1
131 | 14 September 2014, in the afternoon
132 | New
133 |
134 | - Better animations for bubbles
135 |
136 | Fixed
137 |
138 | - Reddit crawler error when reddit times out.
139 |
140 |
141 |
142 |
143 | Version 0.3.0
144 | 14 September 2014
145 | New
146 |
147 | - Customize the minimum score for Reddit or HN sotires
148 | - Customize the number of bubbles to show
149 | - These release notes right here!
150 |
151 | Fixed
152 |
153 | - Slow load time
154 | - Insane crawler frequency for reddit
155 | - Plurals for single values (e.g. '1 comments')
156 | - Chart doesn't load if socket returns before the first fetch
157 | - Chart zoom resets when opening
158 |
159 |
160 |
161 | Pre version 0.3.0
162 | New
163 |
166 | Fixed
167 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/dist/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | About
6 |
15 |
16 |
17 | Back to the bubbles
18 | Release Notes
19 |
20 | Version 0.4.5
21 | 11 October 2014
22 | New
23 |
24 | - Links to YouTube videos in comments will be replaced with in-line YouTube players.
25 | - Hacker News stories now real-time (using the new FireBase API).
26 | - Links to reply to comments for Hacker News and Reddit.
27 | - Pretty new icons.
28 |
29 | Fixed
30 |
31 | - Hacker News bubbles with zero comments are not showing.
32 | - Deleted Reddit comments are showing.
33 | - Vote buttons show when voting isn't possible.
34 |
35 |
36 |
37 |
38 | Version 0.4.0
39 | 7 October 2014
40 | New
41 |
42 | - Sign in with Reddit to vote (comments and comment voting coming soon).
43 |
44 | Fixed
45 |
46 | - Oh heaps of stuff.
47 |
48 |
49 |
50 |
51 | Version 0.3.7
52 | 2 October 2014
53 | New
54 |
55 | - Sign in with Facebook to sync across devices: settings, already read items and favourites.
56 | - Pictures in comments show in-line (only direct links to images for now, not imgur pages, etc.)
57 |
58 | Fixed
59 |
60 | - Database fails to store new stories.
61 | - Tooltip covers circle in IE.
62 | - Tooltip shows on touch devices.
63 | - Header items don't fit on small devices.
64 | - If comments are slow to load, they can take focus of the story panel
65 | - "\n" showing in comments.
66 | - [Deleted] reddit comments showing
67 |
68 |
69 |
70 |
71 | Version 0.3.6
72 | 24 September 2014
73 | New
74 |
75 | - The name. Bye bye 'News bubbles', hello 'Bubble Reader'.
76 |
77 | Fixed
78 |
79 | - Database reached the max allowed size because I'm storing a whole lot of crap that I don't need to be.
80 | - Subreddit links in story comments (e.g. /r/dogs_and_cats) don't work.
81 | - Links in comments open in the same page (yes, I'm calling that a bug).
82 | - Page can take several seconds to load even on fast connection.
83 | - Memory leak on server causes intermittent shutdown.
84 |
85 |
86 |
87 |
88 | Version 0.3.5
89 | 19 September 2014
90 | New
91 |
92 | - Favorites! Star an item in the story panel and it will appear on the favourites page.
93 |
94 | Fixed
95 |
96 | - Right click doesn't send circle to back.
97 | - 'null' shows below images from imgur gallery if there is no title.
98 |
99 |
100 |
101 |
102 | Version 0.3.3
103 | 16 September 2014
104 | New
105 |
106 | - Comments for Reddit stories now show below image/article in the story panel.
107 | - imgur.com galleries display with inline captions.
108 |
109 | Fixed
110 |
113 |
114 |
115 |
116 | Version 0.3.2
117 | 15 September 2014
118 | New
119 |
120 | - Moved hosting from MongoHQ (Compose) to MongoLab.
121 | - Dynamic y-axis exponent keeps bubbles more evenly distributed.
122 |
123 | Fixed
124 |
125 | - Crawler error when forcing a crawl.
126 |
127 |
128 |
129 |
130 | Version 0.3.1
131 | 14 September 2014, in the afternoon
132 | New
133 |
134 | - Better animations for bubbles
135 |
136 | Fixed
137 |
138 | - Reddit crawler error when reddit times out.
139 |
140 |
141 |
142 |
143 | Version 0.3.0
144 | 14 September 2014
145 | New
146 |
147 | - Customize the minimum score for Reddit or HN sotires
148 | - Customize the number of bubbles to show
149 | - These release notes right here!
150 |
151 | Fixed
152 |
153 | - Slow load time
154 | - Insane crawler frequency for reddit
155 | - Plurals for single values (e.g. '1 comments')
156 | - Chart doesn't load if socket returns before the first fetch
157 | - Chart zoom resets when opening
158 |
159 |
160 |
161 | Pre version 0.3.0
162 | New
163 |
166 | Fixed
167 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/app/scripts/events.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.Events = (function() {
5 | var Events = {}
6 | , storyPanel
7 | , chartWrapper
8 | , storyPanelResizer
9 | , offsetX
10 | , body
11 | ;
12 |
13 | function resizerMousedown() {
14 | if (d3.event.target.id === 'story-panel-toggle') { return false; }
15 | chartWrapper = d3.select('#chart-wrapper').style('transition', '0ms');
16 | storyPanel = d3.select('#story-panel').style('transition', '0ms');
17 |
18 | storyPanelResizer = d3.select('#story-panel-resizer').classed('active', true);
19 |
20 | body = d3.select('body');
21 | offsetX = d3.mouse(document.body)[0] - NB.splitPos;
22 |
23 | body.on('mousemove', resizerMousemove);
24 | body.on('mouseup', resizerMouseup);
25 | body.on('touchmove', resizerMousemove);
26 | body.on('touchend', resizerMouseup);
27 | }
28 |
29 | function resizerMousemove() {
30 | d3.event.preventDefault();
31 | NB.splitPos = Math.max(100, d3.mouse(document.body)[0] - offsetX);
32 | NB.Layout.moveSplitPos();
33 | NB.Chart.resize();
34 | }
35 |
36 | function resizerMouseup() {
37 | chartWrapper.style('transition', null);
38 | storyPanel.style('transition', null);
39 | storyPanelResizer.classed('active', false);
40 |
41 | //Snap the splitter to the right if it's less that xpx
42 | if (document.body.offsetWidth - NB.splitPos < 100) {
43 | NB.Layout.hideStoryPanel();
44 | }
45 |
46 | body.on('mousemove', null);
47 | body.on('mouseup', null);
48 | body.on('touchmove', null);
49 | body.on('touchend', null);
50 | }
51 |
52 | // function chartBubbleClicked(d) {
53 |
54 | // //move to back
55 | // moveToBack();
56 |
57 | // //get the D3 flvoured dom el
58 | // var el = d3.select(d3.event.currentTarget);
59 | // //TODO if clicked story is already showing, return. (lastID === d._id)
60 |
61 | // //Make the last selected item read and no longer selected
62 | // d3.select('.selected')
63 | // .classed('selected', false);
64 |
65 | // //Now select the item just clicked
66 | // el.classed('selected', true);
67 |
68 |
69 |
70 | // var setting = NB.Settings.getSetting('clickAction');
71 |
72 | // if (setting === 'storyPanel') {
73 | // NB.Data.markAsRead(d._id);
74 | // el.classed('read', true);
75 | // NB.Layout.showStoryPanel();
76 | // NB.StoryPanel.render(d);
77 | // }
78 |
79 | // if (setting === 'storyTooltip') { //TODO move this out to maxiTooltip module (pass el and d)
80 | // var maxiTooltip = d3.select('#story-tooltip');
81 | // var tooltipWidth = parseInt(maxiTooltip.style('width'));
82 | // var tooltipHeight = parseInt(maxiTooltip.style('height'));
83 |
84 | // var thisDims = el.node().getBoundingClientRect();
85 |
86 | // var r = z(d.commentCount);
87 | // var left = thisDims.left + r - tooltipWidth / 2;
88 | // var maxLeft = w - tooltipWidth - 20;
89 | // left = Math.min(left, maxLeft);
90 | // left = Math.max(left, 0);
91 |
92 | // var top = thisDims.top - tooltipHeight;
93 | // if (top < 50) {
94 | // top = thisDims.bottom;
95 | // }
96 |
97 | // NB.StoryModel.setCurrentStory('tooltip', d); //TODO should this make visible? E.g. control vis in model?
98 |
99 | // var readUnreadLink = d3.select('#tooltip-mark-as-read');
100 | // if (el.classed('read')) {
101 | // readUnreadLink.text('Mark as unread');
102 | // } else {
103 | // readUnreadLink.text('Mark as read');
104 | // }
105 |
106 |
107 | // var duration = maxiTooltipShowing ? 200 : 0;
108 | // maxiTooltip
109 | // .style('display', 'block')
110 | // .transition()
111 | // .duration(duration)
112 | // .style('left', left + 'px')
113 | // .style('top', top + 'px');
114 |
115 | // maxiTooltipShowing = true; //will block little tooltip from showing
116 |
117 | // d3.event.stopPropagation(); //TODO I do not know the diff between this and immediate
118 |
119 | // $(document).on('click.tooltip', function() { //TODO try .one, still not working?
120 | // maxiTooltip.style('display', 'none');
121 | // $(document).off('click.tooltip');
122 |
123 | // window.setTimeout(function() {
124 | // maxiTooltipShowing = false; //wait a bit before allowing the little tooltip to show
125 | // }, 300);
126 | // });
127 | // readUnreadLink.on('click', function() { //D3 will remove any existing listener
128 | // toggleRead(el, d);
129 | // });
130 | // d3.select('#tooltip-open-reading-pane').on('click', function() { //D3 will remove any existing listener
131 | // NB.Data.markAsRead(d.id);
132 | // el.classed('read', true);
133 | // NB.Layout.showStoryPanel();
134 | // NB.StoryPanel.render(d);
135 | // });
136 |
137 | // }
138 |
139 | // if (setting === 'openTab') {
140 | // //TODO I'm not sure I can do this, maybe the text should be 'open page' or 'navigate to URL'
141 | // }
142 | // tooltip.style('visibility', 'hidden');
143 | // }
144 |
145 |
146 | function init() {
147 | window.onresize = function() {
148 | NB.Layout.render();
149 | NB.Chart.resize();
150 | };
151 |
152 | d3.select('#story-panel-resizer').on('mousedown', resizerMousedown);
153 | d3.select('#story-panel-resizer').on('touchstart', resizerMousedown);
154 |
155 |
156 | d3.select('#story-panel-toggle').on('click', function() {
157 | d3.event.preventDefault();
158 | NB.Layout.toggleStoryPanel();
159 | return false;
160 | });
161 |
162 | $('#more-btn').on('click', function() {
163 | NB.Data.getNextPage(function(data) {
164 | NB.Chart.addStories(data);
165 | });
166 | });
167 |
168 | }
169 |
170 |
171 |
172 | /* --------------- */
173 | /* -- Exports -- */
174 | /* --------------- */
175 |
176 |
177 | init();
178 | return Events;
179 |
180 | })();
--------------------------------------------------------------------------------
/server/rdtCrawler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path')
4 | , request = require('request')
5 | , storyController = require(path.join(__dirname, 'controllers', 'story.controller'))
6 | , utils = require(path.join(__dirname, 'utils'))
7 | , devLog = utils.devLog
8 | , prodLog = utils.prodLog
9 | , getInProgress = false
10 | ;
11 |
12 | function upsert(story) {
13 | process.nextTick(function() {
14 | storyController.upsertRdtStory(story);
15 | });
16 | }
17 |
18 | function saveStories(data) {
19 | // devLog(' -- Saving', data.length, 'RDT stories --');
20 | data.forEach(function(story) {
21 | upsert(story);
22 | });
23 | }
24 |
25 |
26 | function goGet(url, cb) {
27 | var options = {
28 | url: url,
29 | json: true,
30 | 'User-Agent': 'news-bubbles.herokuapp.com/0.4.0 by /u/bubble_boi'
31 | };
32 |
33 | request.get(options, function(err, response, data) {
34 | if (!response) { return; }
35 | if (response.headers['X-Ratelimit-Used']) {
36 | prodLog('X-Ratelimit-Used' + response.headers['X-Ratelimit-Used']);
37 | }
38 | if (response.headers['X-Ratelimit-Remaining']) {
39 | prodLog('X-Ratelimit-Remaining' + response.headers['X-Ratelimit-Remaining']);
40 | }
41 | if (response.headers['X-Ratelimit-Remaining']) {
42 | prodLog('X-Ratelimit-Reset' + response.headers['X-Ratelimit-Reset']);
43 | }
44 | // devLog('got data:');
45 | // console.log(data);
46 | cb(data);
47 | });
48 | }
49 |
50 | function buildUrl(props) {
51 | props = props || {};
52 | var url = 'http://www.reddit.com/' + (props.list || 'new') + '.json';
53 | url += '?limit=' + (props.limit || 100);
54 | url += props.after ? '&after=' + props.after : '';
55 |
56 | return url;
57 | }
58 |
59 | function startCrawler() {
60 | prodLog('Starting Reddit crawler');
61 |
62 | /* -- looper variables -- */
63 | //'new' loopers
64 | var loopers = [
65 | {
66 | name: 'Looper 1',
67 | list: 'new',
68 | count: 0,
69 | interval: 11000,
70 | loops: 15,
71 | lastKnownAfter: undefined
72 | },
73 | {
74 | name: 'Looper 2',
75 | list: 'new',
76 | count: 0,
77 | interval: 31000,
78 | loops: 30,
79 | lastKnownAfter: undefined
80 | },
81 | {
82 | name: 'Looper 3',
83 | list: 'new',
84 | count: 0,
85 | interval: 61000,
86 | loops: 60,
87 | lastKnownAfter: undefined
88 | },
89 | {
90 | name: 'Looper 4',
91 | list: 'new',
92 | count: 0,
93 | interval: 127000,
94 | loops: 120,
95 | lastKnownAfter: undefined
96 | },
97 | // {
98 | // name: 'Looper 5',
99 | // list: 'new',
100 | // count: 0,
101 | // interval: 241000,
102 | // loops: 240,
103 | // lastKnownAfter: undefined
104 | // },
105 |
106 | //'hot' loopers
107 | {
108 | name: 'Looper 6',
109 | list: 'hot',
110 | count: 0,
111 | interval: 13000,
112 | loops: 15,
113 | lastKnownAfter: undefined
114 | },
115 | {
116 | name: 'Looper 7',
117 | list: 'hot',
118 | count: 0,
119 | interval: 29000,
120 | loops: 30,
121 | lastKnownAfter: undefined
122 | },
123 | {
124 | name: 'Looper 8',
125 | list: 'hot',
126 | count: 0,
127 | interval: 63000,
128 | loops: 60,
129 | lastKnownAfter: undefined
130 | },
131 | {
132 | name: 'Looper 9',
133 | list: 'hot',
134 | count: 0,
135 | interval: 123000,
136 | loops: 120,
137 | lastKnownAfter: undefined
138 | },
139 | // {
140 | // name: 'Looper 10',
141 | // list: 'hot',
142 | // count: 0,
143 | // interval: 240000,
144 | // loops: 240,
145 | // lastKnownAfter: undefined
146 | // }
147 | ];
148 |
149 | function fetch(looper) {
150 | // devLog(looper.name + ' - getting...');
151 | var url = buildUrl({after: looper.lastKnownAfter, list: looper.list});
152 |
153 | goGet(url, function(response) {
154 | getInProgress = false;
155 | try {
156 | if (response.data) { //this should save the try, but who knows.
157 | saveStories(response.data.children);
158 | looper.lastKnownAfter = response.data.after;
159 | } else {
160 | prodLog('The reddit response did not have data. It looks like this:');
161 | console.log(response);
162 | }
163 | } catch (err) {
164 | prodLog('Error in reddit crawler:', err);
165 | looper.count = 0;
166 | looper.lastKnownAfter = undefined;
167 | }
168 | });
169 | }
170 |
171 | function startLooper(looper) {
172 | setInterval(function() {
173 | if (getInProgress) {
174 | // prodLog('There is a get already in progress, skipping this loop');
175 | return;
176 | } //I think overlapping might be causing problems
177 | getInProgress = true;
178 | if (looper.count > looper.loops) {
179 | looper.count = 0;
180 | looper.lastKnownAfter = undefined;
181 | } else {
182 | looper.count++;
183 | }
184 |
185 | //TODO do I need nextTick here?
186 | // process.nextTick(function() {
187 | fetch(looper);
188 | // });
189 |
190 | }, looper.interval);
191 | }
192 |
193 | for (var i = 0; i < loopers.length; i++) {
194 | startLooper(loopers[i]);
195 | }
196 | }
197 |
198 | exports.startCrawler = startCrawler;
199 |
200 |
201 | exports.forceFetch = function(req, res) {
202 | var loops = req.params.limit / 100
203 | , count = 0
204 | , lastKnownAfter
205 | , url = '';
206 |
207 | function go() {
208 | url = buildUrl({after: lastKnownAfter, list: req.params.list});
209 | // devLog('Getting data with the URL:', url);
210 |
211 | devLog('tick', count);
212 | goGet(url, function(response) {
213 | res.send('Forced reddit crawl');
214 |
215 | if (response.data && response.data.children) {
216 | saveStories(response.data.children);
217 | lastKnownAfter = response.data.after;
218 | }
219 |
220 | });
221 | }
222 |
223 | var interval = setInterval(function() {
224 | if (count >= loops) {
225 | devLog('done');
226 | clearInterval(interval);
227 | } else {
228 | count++;
229 | go();
230 | }
231 |
232 | }, 2000);
233 | };
--------------------------------------------------------------------------------
/server/hxnCrawler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //Hacker News
4 | //https://hn.algolia.com/api
5 |
6 | var path = require('path')
7 | , request = require('request')
8 | , storyController = require(path.join(__dirname, 'controllers', 'story.controller'))
9 | , utils = require(path.join(__dirname, 'utils'))
10 | , devLog = utils.devLog
11 | , prodLog = utils.prodLog
12 | , HITS_PER_PAGE_LIMIT = 1000
13 | , MIN_POINTS = 0
14 |
15 | , oneMin = 60
16 | , oneHour = 60 * 60
17 | , oneDay = 24 * 60 * 60
18 |
19 | //Intervals (milliseconds)
20 | , every10Secs = 1000 * 10
21 | , every1Min = 1000 * 60
22 | , every5Mins = 1000 * 60 * 5
23 | , every10Mins = 1000 * 60 * 10
24 | , every20Mins = 1000 * 60 * 20
25 | , every30Mins = 1000 * 60 * 30
26 | , every1Day = 1000 * 60 * 60 * 24
27 | ;
28 |
29 |
30 |
31 | /* --------------------------- */
32 | /* -- HACKER NEWS CRAWLER -- */
33 | /* --------------------------- */
34 |
35 | function goGet(url, cb) {
36 | request.get({url: url, json: true}, function(err, req, data) {
37 | cb(data);
38 | });
39 | }
40 |
41 |
42 |
43 | //TODO this probably belongs in controllers, but don't want callback soup or passing io around everywhere right now
44 | function saveStories(data, suppressResults) {
45 | try {
46 | // devLog(' -- Saving', data.hits.length, 'HXN stories --');
47 | if (!data) { return; }
48 | var stories = data.hits;
49 | // var newOrUpdatedStories = [];
50 | // var savedStories = 0;
51 | stories.forEach(function(story) {
52 | storyController.upsertHxnStory(story, suppressResults);
53 | });
54 | } catch (err) {
55 | devLog('Error saving HXN stories:', err);
56 | }
57 | }
58 |
59 | function buildUrl(props) {
60 | var url = 'https://hn.algolia.com/api/v1/';
61 | url += 'search_by_date?';
62 | url += 'tags=(story,show_hn,ask_hn)';
63 | url += '&hitsPerPage=' + (props.hitsPerPage || HITS_PER_PAGE_LIMIT);
64 | url += props.page ? '&page=' + props.page : '';
65 | url += '&numericFilters=created_at_i>' + props.minDate + ',created_at_i<' + props.maxDate;
66 | url += ',points>' + (props.minPoints || MIN_POINTS);
67 |
68 | return url;
69 | }
70 |
71 |
72 | //Force get the last 1000 stories over 1 point. Handy if the server goes down or something.
73 | exports.forceFetch = function(req, res) {
74 | var now = new Date().getTime() / 1000;
75 | var url = buildUrl({minDate: 0, maxDate: now, minPoints: 1});
76 |
77 | goGet(url, function(data) {
78 | saveStories(data);
79 | res.send('Forced hacker news crawl');
80 | });
81 | };
82 |
83 |
84 | exports.startCrawler = function() {
85 | prodLog('Starting Hacker News crawler!');
86 | // io.emit('data update', {data: 'yes, there will totally be data here'});
87 |
88 | //Get stories from last 30 mins
89 | setInterval(function() {
90 | var now = new Date().getTime() / 1000;
91 | var url = buildUrl({minDate: now - oneMin * 30, maxDate: now});
92 |
93 | goGet(url, function(data) {
94 | saveStories(data);
95 | });
96 | }, every10Secs); //TODO this should not be uncommented in prod
97 | // }, every1Min);
98 |
99 | //Get stories from 30 mins to 2 hours
100 | setTimeout(function() {
101 | setInterval(function() {
102 | var now = new Date().getTime() / 1000;
103 | var url = buildUrl({minDate: now - oneHour * 2, maxDate: now - oneMin * 30});
104 | goGet(url, function(data) {
105 | saveStories(data);
106 | });
107 | }, every5Mins);
108 | }, 10000); //stagger
109 |
110 | // //Get stories from 2 to 6 hours
111 | setTimeout(function() {
112 | setInterval(function() {
113 | var now = new Date().getTime() / 1000;
114 | var url = buildUrl({minDate: now - oneHour * 6, maxDate: now - oneHour * 2});
115 |
116 | goGet(url, function(data) {
117 | saveStories(data);
118 | });
119 | }, every10Mins);
120 | }, 20000); //stagger
121 |
122 | //Get stories from 6 to 12 hours
123 | setTimeout(function() {
124 | setInterval(function() {
125 | var now = new Date().getTime() / 1000;
126 | var url = buildUrl({minDate: now - oneHour * 12, maxDate: now - oneHour * 6});
127 |
128 | goGet(url, function(data) {
129 | saveStories(data);
130 | });
131 | }, every20Mins);
132 | }, 30000); //stagger
133 |
134 |
135 | //Get stories from 12 to 24 hours
136 | setTimeout(function() {
137 | setInterval(function() {
138 | var now = new Date().getTime() / 1000;
139 | var url = buildUrl({minDate: now - oneHour * 24, maxDate: now - oneHour * 12});
140 |
141 | goGet(url, function(data) {
142 | saveStories(data);
143 | });
144 | }, every30Mins);
145 | }, 40000); //stagger
146 |
147 |
148 |
149 |
150 |
151 | /* -- The below run daily and get older stories over a certain number of points -- */
152 |
153 | //Get stories from 1 to 30 days over 100 points
154 | setTimeout(function() {
155 | setInterval(function() {
156 | var now = new Date().getTime() / 1000;
157 | var url = buildUrl({minDate: now - oneDay * 30, maxDate: now - oneDay, minPoints: 100});
158 |
159 | goGet(url, function(data) {
160 | saveStories(data, true);
161 | });
162 | }, every1Day);
163 | }, 50000); //stagger
164 |
165 | //Get stories from 30 to 90 days over 150 points
166 | setTimeout(function() {
167 | setInterval(function() {
168 | var now = new Date().getTime() / 1000;
169 | var url = buildUrl({minDate: now - oneDay * 90, maxDate: now - oneDay * 30, minPoints: 150});
170 |
171 | goGet(url, function(data) {
172 | saveStories(data, true);
173 | });
174 | }, every1Day);
175 | }, 50000); //stagger
176 |
177 | //Get stories from 90 to 200 days over 200 points
178 | setTimeout(function() {
179 | setInterval(function() {
180 | var now = new Date().getTime() / 1000;
181 | var url = buildUrl({minDate: now - oneDay * 200, maxDate: now - oneDay * 90, minPoints: 150});
182 |
183 | goGet(url, function(data) {
184 | saveStories(data, true);
185 | });
186 | }, every1Day);
187 | }, 50000); //stagger
188 |
189 | //Get stories from 200 to 365 days over 250 points
190 | setTimeout(function() {
191 | setInterval(function() {
192 | var now = new Date().getTime() / 1000;
193 | var url = buildUrl({minDate: now - oneDay * 365, maxDate: now - oneDay * 200, minPoints: 250});
194 |
195 | goGet(url, function(data) {
196 | saveStories(data, true);
197 | });
198 | }, every1Day);
199 | }, 50000); //stagger
200 |
201 | //Get stories from 365 to 600 days over 300 points
202 | setTimeout(function() {
203 | setInterval(function() {
204 | var now = new Date().getTime() / 1000;
205 | var url = buildUrl({minDate: now - oneDay * 600, maxDate: now - oneDay * 365, minPoints: 300});
206 |
207 | goGet(url, function(data) {
208 | saveStories(data, true);
209 | });
210 | }, every1Day);
211 | }, 50000); //stagger
212 |
213 | //Get stories over 600 days old and over 400 points
214 | setTimeout(function() {
215 | setInterval(function() {
216 | var now = new Date().getTime() / 1000;
217 | var url = buildUrl({minDate: 0, maxDate: now - oneDay * 600, minPoints: 400});
218 |
219 | goGet(url, function(data) {
220 | saveStories(data, true);
221 | });
222 | }, every1Day);
223 | }, 50000); //stagger
224 |
225 | };
226 |
227 |
228 |
--------------------------------------------------------------------------------
/app/scripts/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.Settings = (function() {
5 |
6 | var Settings = {}
7 | , settings = {}
8 | , settingsEl
9 | ;
10 |
11 | function retrieveLocalSettings() {
12 | if (localStorage.settings) {
13 | var localSettings = JSON.parse(localStorage.settings);
14 |
15 | if (localSettings.clickAction) { Settings.clickAction(localSettings.clickAction); }
16 | if (localSettings.rightClickAction) { Settings.rightClickAction(localSettings.rightClickAction); }
17 | if (localSettings.source) {
18 | //TODO replace this logic with versioning the localstorage
19 | if (localSettings.source === 'rd') { localSettings.source = 'rdt'; }
20 | if (localSettings.source === 'hn') { localSettings.source = 'hxn'; }
21 | Settings.source(localSettings.source);
22 | }
23 | if (localSettings.hitLimit) { Settings.hitLimit(+localSettings.hitLimit); }
24 | if (localSettings.rdtMinScore) { Settings.rdtMinScore(+localSettings.rdtMinScore); }
25 | if (localSettings.hxnMinScore) { Settings.hxnMinScore(+localSettings.hxnMinScore); }
26 |
27 | }
28 | }
29 |
30 | function init() {
31 | var defaultPage = 'rdt';
32 | if (window.location.hash === '#hxn') {
33 | defaultPage = 'hxn';
34 | if (window.history && window.history.replaceState) {
35 | window.history.replaceState('', document.title, window.location.pathname);
36 | } else {
37 | window.location.hash = '';
38 | }
39 | }
40 |
41 | //TODO why are these not just bound with KO?
42 | d3.select('#open-settings-btn').on('click', Settings.openSettings);
43 | d3.select('#save-settings-btn').on('click', Settings.saveSettings);
44 | d3.select('#cancel-settings-btn').on('click', Settings.cancelSettings);
45 |
46 | //Init a settings objects with some defaults.
47 | Settings.clickAction = ko.observable('storyPanel'); //storyPanel | storyTooltip
48 | Settings.rightClickAction = ko.observable('toggleRead'); // toggleRead | nothing
49 | Settings.source = ko.observable(defaultPage); // rdt | hxn
50 | Settings.hitLimit = ko.observable(100);
51 | Settings.rdtMinScore = ko.observable(500);
52 | Settings.hxnMinScore = ko.observable(5);
53 | Settings.favMinScore = ko.observable(0);
54 | //TODO this will need to be universal so that favourites will be coloured correctly.
55 | Settings.hxnCategoryColors = ko.observableArray([
56 | {category: 'Ask HN', color: '#e74c3c'},
57 | {category: 'Show HN', color: '#16a085'},
58 | {category: 'Everything else', color: '#2980b9'}
59 | ]);
60 | Settings.rdtCategoryColors = ko.observableArray([
61 | {category: 'AskReddit', color: '#2980b9'},
62 | {category: 'funny', color: '#2ecc71'},
63 | {category: 'pics', color: '#e67e22'},
64 | {category: 'aww', color: '#8e44ad'},
65 | {category: 'videos', color: '#e74c3c'},
66 | {category: 'Showerthoughts', color: '#f1c40f'},
67 | {category: 'Everything else', color: '#7f8c8d'}
68 | ]);
69 |
70 | settingsEl = d3.select('#settings-modal');
71 |
72 | // ko.applyBindings(settings, settingsEl.node(0));
73 |
74 | retrieveLocalSettings(); //Override the defaults if they were in local storage.
75 |
76 | }
77 |
78 | function closeSettings() {
79 | settingsEl
80 | .transition().duration(500)
81 | .style('opacity', 0)
82 | .transition()
83 | .style('display', 'none');
84 | }
85 |
86 | function saveSettings(silent) {
87 | if (!silent) {
88 | NB.Data.emit('updateSettings', {settings: ko.toJS(Settings)});
89 | }
90 |
91 | //The settings ko object is bound so nothing needs to be updated there
92 | var tmp = NB.Utils.constrain(1, Settings.hitLimit(), 500);
93 | Settings.hitLimit(tmp);
94 |
95 | tmp = Math.max(0, Settings.rdtMinScore());
96 | Settings.rdtMinScore(tmp);
97 |
98 | tmp = Math.max(0, Settings.hxnMinScore());
99 | Settings.hxnMinScore(tmp);
100 |
101 | var localSettings = {
102 | clickAction: Settings.clickAction(),
103 | rightClickAction: Settings.rightClickAction(),
104 | source: Settings.source(),
105 | hitLimit: Settings.hitLimit(),
106 | rdtMinScore: Settings.rdtMinScore(),
107 | hxnMinScore: Settings.hxnMinScore()
108 | };
109 |
110 | var previousSettings = {};
111 |
112 | if (localStorage.settings) {
113 | previousSettings = JSON.parse(localStorage.settings);
114 | }
115 |
116 | if (Settings.hitLimit() !== previousSettings.hitLimit) {
117 | NB.Chart.reset();
118 | NB.Data.getData();
119 | }
120 |
121 | var src = Settings.source();
122 | var koScore = Settings[src + 'MinScore'];
123 | if (koScore && koScore() !== previousSettings[src + 'MinScore']) {
124 | NB.Chart.reset();
125 | NB.Data.getData();
126 | }
127 | //TODO if hxn or rdt limits changed...
128 |
129 | localStorage.settings = JSON.stringify(localSettings);
130 | closeSettings();
131 | }
132 |
133 | function setAll(settings) {
134 | var keys = Object.keys(settings);
135 | keys.forEach(function(setting) {
136 | //TODO just save the settings directly here, but don't emit saved changes after
137 | Settings.setSetting(setting, settings[setting], true);
138 | });
139 | }
140 |
141 |
142 | function openSettings() {
143 | settingsEl
144 | .style('display', 'block')
145 | .transition().duration(100)
146 | .style('opacity', 1);
147 | }
148 |
149 | function cancelSettings() {
150 | //since the settings object is bound to the radio buttons, it may have changed.
151 | //so reset it to what's in localStorage
152 | retrieveLocalSettings();
153 | closeSettings();
154 | }
155 |
156 | function getSetting(setting) { //TODO this will be redundant soon with direct access
157 | if (!Settings[setting]) {
158 | console.log(setting + ' is not a setting.');
159 | return;
160 | }
161 | return Settings[setting]();
162 | }
163 |
164 | function setSetting(setting, value, silent) {
165 | //TODO, if this took an object, then I could use Object.keys and merge this with setAll.
166 | if (!Settings[setting]) { //TODO test for "typeof function"
167 | console.log(setting + ' is not something that can be set.');
168 | return;
169 | }
170 | Settings[setting](value);
171 | saveSettings(silent);
172 | }
173 |
174 | function getColor(source, category) {
175 | if (!Settings[source + 'CategoryColors']) {
176 | console.log('There are no colours for this source');
177 | return;
178 | }
179 | var arr = Settings[source + 'CategoryColors']();
180 | var defaultColor;
181 | for (var i = 0; i < arr.length; i++) {
182 | if (arr[i].category === category) {
183 | return arr[i].color;
184 | }
185 | if (arr[i].category === 'Everything else') {
186 | defaultColor = arr[i].color;
187 | }
188 | }
189 | return defaultColor;
190 | }
191 |
192 |
193 |
194 |
195 | /* --------------- */
196 | /* -- Exports -- */
197 | /* --------------- */
198 |
199 | Settings.openSettings = openSettings;
200 | Settings.saveSettings = saveSettings;
201 | Settings.cancelSettings = cancelSettings;
202 | Settings.getSetting = getSetting;
203 | Settings.setAll = setAll;
204 | Settings.setSetting = setSetting;
205 | Settings.getColor = getColor;
206 |
207 | init();
208 | return Settings;
209 |
210 | })();
211 |
212 |
--------------------------------------------------------------------------------
/app/scripts/storyPanel.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var NB = NB || {};
3 |
4 | NB.StoryPanel = (function() {
5 | var StoryPanel = {};
6 | var currentStoryId;
7 | var currentStory;
8 |
9 | function getGifvHtml(gifVUrl) {
10 | var webm = gifVUrl.replace(/.gifv$/, '.webm');
11 | var mp4 = gifVUrl.replace(/.gifv$/, '.mp4');
12 |
13 | var html = [
14 | '',
15 | '',
19 | ''
20 | ].join('');
21 |
22 | return html;
23 | }
24 |
25 |
26 | function getReadability(story, cb) {
27 | var urlBase = '/readability/'
28 | , pageUrl = story.url
29 | , fullUrl = urlBase + encodeURIComponent(pageUrl);
30 |
31 | //TODO remove jQuery
32 | $.get(fullUrl, function(data) {
33 |
34 | if (data.error) {
35 | var msg = [
36 | 'Whoa!
',
37 | 'This is far too good for this little panel. Better go see the whole thing ',
38 | 'here.',
39 | '
'
40 | ].join('');
41 | cb(msg);
42 | } else {
43 | cb(data.content);
44 | }
45 |
46 | });
47 | }
48 |
49 |
50 | function renderRdt(story) {
51 | var dom = story.rdt.domain.toLowerCase();
52 | currentStoryId = story.sourceId; //To check when comments come back
53 | story.content = '';
54 |
55 | //get comments and append. NB done() is not needed.
56 | function appendComments() {
57 | NB.Comments.getForRdtStory(story, function(commentTree) {
58 | story.content += '';
59 | story.content += [
60 | ''
63 | ].join('');
64 | story.content += commentTree.html();
65 |
66 | //Because a user can click one story, then another before the first story comments are loaded
67 | //Check that the expected story is still the active one.
68 | if (story.sourceId === currentStoryId) {
69 | NB.StoryModel.setCurrentStory(story);
70 | } else {
71 | console.log('The story has already changed, dumping these comments');
72 | }
73 |
74 | });
75 | }
76 |
77 | function done(thenAppendComments) {
78 | NB.StoryModel.setCurrentStory(story);
79 | if (thenAppendComments) {
80 | appendComments();
81 | }
82 | }
83 |
84 |
85 | if (story.rdt.self) {
86 | NB.Comments.getForRdtStory(story, function(commentTree) {
87 | story.content = commentTree.html();
88 | done();
89 | });
90 |
91 | } else if (story.url.match(/(gif|png|jpg)$/)) { //any old image link, might be imgur
92 | story.content = '
';
93 | done(true);
94 | } else if (story.url.match(/gifv$/)) { //any old image link, might be imgur
95 | story.content = getGifvHtml(story.url);
96 | done(true);
97 | } else if (dom === 'i.imgur.com' || dom === 'imgur.com' || dom === 'm.imgur.com') { //TODO does m. exist, and obviously regex
98 |
99 | if (story.url.match(/\imgur\.com\/a\//)) { //it is an imgur album (/a/)
100 | var albumId = story.url.replace(/.*?\imgur\.com\/a\//, '');
101 | albumId = albumId.replace(/#.*/, ''); //remove trailing hash
102 | albumId = albumId.replace(/\?.*/, ''); //remove trailing query string
103 | var url = 'https://api.imgur.com/3/album/' + albumId + '/images';
104 | var html = '';
105 | $.get(url, function(response) {
106 | html += '';
107 |
108 | response.data.forEach(function(img) {
109 | html += '

';
110 | });
111 |
112 | html += '
';
113 |
114 | story.content = html;
115 | done(true);
116 |
117 | });
118 | } else if (story.url.match(/\imgur\.com\/gallery\//)) {
119 | var id = story.url.match(/imgur\.com\/gallery\/([^?]*)/)[1];
120 |
121 | NB.Data.getImgurGalleryAsHtml(id, function(html) {
122 | story.content = '' + html + '
';
123 | done(true);
124 | });
125 |
126 | } else {
127 | var imgUrl = story.url.replace('imgur.com', 'i.imgur.com') + '.jpg';
128 |
129 | story.content = [
130 | '',
131 | '

',
132 | '
'
133 | ].join('');
134 | done(true);
135 | }
136 |
137 | } else {
138 |
139 | getReadability(story, function(content) {
140 | story.content = content;
141 | done(true);
142 | });
143 |
144 | }
145 |
146 |
147 | } //END renderRdt
148 |
149 |
150 |
151 |
152 | function renderHxn(story) {
153 |
154 | function appendComments() {
155 | NB.Comments.getForHxnStory(story, function(commentTree) {
156 | story.content += '';
157 | story.content += commentTree;
158 | NB.StoryModel.setCurrentStory(story);
159 | });
160 | }
161 |
162 | function done(thenAppendComments) {
163 | NB.StoryModel.setCurrentStory(story);
164 | if (thenAppendComments) {
165 | appendComments();
166 | }
167 | }
168 |
169 | if (story.url) {
170 | if (story.url.match(/pdf\?*.*$/)) {
171 | story.content = 'Open this PDF';
172 | done(true);
173 | // NB.StoryModel.setCurrentStory('panel', story);
174 |
175 | } else {
176 | getReadability(story, function(content) {
177 | story.content = content;
178 | done(true);
179 | // NB.StoryModel.setCurrentStory('panel', story);
180 |
181 | });
182 | }
183 | } else {
184 | NB.Comments.getForHxnStory(story, function(commentTree) {
185 | story.content = commentTree;
186 | done(false);
187 | // NB.StoryModel.setCurrentStory('panel', story);
188 | });
189 |
190 | }
191 | }
192 |
193 |
194 |
195 |
196 | function render(story) {
197 | currentStory = story;
198 | NB.StoryModel.setCurrentStory(story); //to get a quick change in the panel.
199 |
200 | //The story panel element is passed into these funciton because if it goes to readability it's an async call
201 | //and I don't want to mess around with cbs everywhere
202 | if (story.source === 'rdt') {
203 | renderRdt(story);
204 | }
205 |
206 | if (story.source === 'hxn') {
207 | renderHxn(story);
208 | }
209 |
210 | }
211 |
212 | function clear() {
213 | NB.StoryModel.clear();
214 | }
215 |
216 |
217 | /* --------------- */
218 | /* -- Exports -- */
219 | /* --------------- */
220 |
221 | StoryPanel.render = render;
222 | StoryPanel.clear = clear;
223 |
224 | return StoryPanel;
225 | })();
226 |
--------------------------------------------------------------------------------
/app/scripts/comments.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var NB = NB || {};
4 |
5 | NB.Comments = (function() {
6 | var Comments = {};
7 |
8 | function parseHtml(str) {
9 | var result = $('