├── Procfile ├── HNMobile ├── .bowerrc ├── ionic.project ├── icon.png ├── hnsplash.png ├── www │ ├── img │ │ └── ionic.png │ ├── lib │ │ ├── ionic │ │ │ ├── fonts │ │ │ │ ├── ionicons.eot │ │ │ │ ├── ionicons.ttf │ │ │ │ └── ionicons.woff │ │ │ ├── version.json │ │ │ ├── scss │ │ │ │ ├── _progress.scss │ │ │ │ ├── _backdrop.scss │ │ │ │ ├── ionicons │ │ │ │ │ ├── ionicons.scss │ │ │ │ │ └── _ionicons-font.scss │ │ │ │ ├── ionic.scss │ │ │ │ ├── _loading.scss │ │ │ │ ├── _slide-box.scss │ │ │ │ ├── _button-bar.scss │ │ │ │ ├── _menu.scss │ │ │ │ ├── _animations.scss │ │ │ │ ├── _radio.scss │ │ │ │ ├── _badge.scss │ │ │ │ ├── _platform.scss │ │ │ │ ├── _spinner.scss │ │ │ │ ├── _popup.scss │ │ │ │ ├── _modal.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _refresher.scss │ │ │ │ ├── _select.scss │ │ │ │ ├── _grid.scss │ │ │ │ ├── _type.scss │ │ │ │ ├── _action-sheet.scss │ │ │ │ ├── _popover.scss │ │ │ │ ├── _range.scss │ │ │ │ ├── _transitions.scss │ │ │ │ └── _checkbox.scss │ │ │ └── js │ │ │ │ └── angular │ │ │ │ ├── angular-resource.min.js │ │ │ │ └── angular-sanitize.min.js │ │ └── human-time.js │ ├── templates │ │ ├── playlist.html │ │ ├── top-stories.html │ │ ├── most-points.html │ │ ├── most-recent.html │ │ ├── most-comments.html │ │ ├── about.html │ │ ├── comments.html │ │ ├── menu.html │ │ └── personal.html │ ├── index.html │ ├── js │ │ ├── partials │ │ │ └── story.html │ │ ├── services │ │ │ ├── followers.js │ │ │ └── links.js │ │ └── app.js │ └── css │ │ └── style.css ├── resources │ ├── icon.png │ ├── splash.png │ ├── ios │ │ ├── icon │ │ │ ├── icon.png │ │ │ ├── icon-40.png │ │ │ ├── icon-50.png │ │ │ ├── icon-60.png │ │ │ ├── icon-72.png │ │ │ ├── icon-76.png │ │ │ ├── icon@2x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-50@2x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-72@2x.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-small.png │ │ │ ├── icon-small@2x.png │ │ │ └── icon-small@3x.png │ │ └── splash │ │ │ ├── Default-667h.png │ │ │ ├── Default-736h.png │ │ │ ├── Default~iphone.png │ │ │ ├── Default@2x~iphone.png │ │ │ ├── Default-568h@2x~iphone.png │ │ │ ├── Default-Landscape-736h.png │ │ │ ├── Default-Landscape~ipad.png │ │ │ ├── Default-Portrait~ipad.png │ │ │ ├── Default-Portrait@2x~ipad.png │ │ │ └── Default-Landscape@2x~ipad.png │ └── android │ │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png ├── HackerNewsMobile-1.apk ├── HackerNewsMobile-2.apk ├── HackerNewsMobile-3.apk ├── HackerNewsMobile-4.apk ├── HackerNewsMobile-5.apk ├── my-release-key.keystore ├── bower.json ├── .gitignore ├── package.json ├── scss │ └── ionic.app.scss ├── gulpfile.js ├── config.xml └── hooks │ ├── after_prepare │ └── 010_add_platform_class.js │ └── README.md ├── favicon.ico ├── .gitignore ├── server ├── cache │ ├── cacheRoutes.js │ ├── cacheController.js │ └── cacheModel.js ├── graph │ ├── graphRoutes.js │ ├── graphModel.js │ └── graphController.js ├── users │ ├── userRoutes.js │ ├── userController.js │ └── userModel.js ├── utils │ └── checkAuth.js ├── server.js └── config │ └── middleware.js ├── public ├── app │ ├── topStories │ │ ├── topStories.html │ │ └── topStories.js │ ├── currentlyFollowing │ │ ├── currentlyFollowing.html │ │ └── currentlyFollowing.js │ ├── personal │ │ ├── personal.js │ │ └── personal.html │ ├── tabs │ │ ├── tabs.html │ │ └── tabs.js │ ├── dashboard │ │ ├── dashboard.html │ │ └── dashboard.js │ ├── services │ │ ├── auth.js │ │ ├── links.js │ │ ├── followers.js │ │ ├── dashboard.js │ │ └── graph.js │ ├── partials │ │ └── story.html │ ├── auth │ │ ├── auth.html │ │ └── auth.js │ └── app.js ├── lib │ ├── angular-jwt.min.js │ ├── angular-jwt.js │ ├── human-time.js │ └── angular-route.min.js └── index.html ├── .jshintrc ├── index.js ├── protractor_conf.js ├── bower.json ├── test-main.js ├── README.md ├── test ├── e2e │ └── generalSpec.js └── clientSpec │ ├── serviceSpec.js │ └── controllerSpec.js ├── package.json ├── karma.conf.js └── _PRESS-RELEASE.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /HNMobile/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "www/lib" 3 | } 4 | -------------------------------------------------------------------------------- /HNMobile/ionic.project: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HNMobile", 3 | "app_id": "" 4 | } -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/favicon.ico -------------------------------------------------------------------------------- /HNMobile/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/icon.png -------------------------------------------------------------------------------- /HNMobile/hnsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/hnsplash.png -------------------------------------------------------------------------------- /HNMobile/www/img/ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/www/img/ionic.png -------------------------------------------------------------------------------- /HNMobile/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/icon.png -------------------------------------------------------------------------------- /HNMobile/HackerNewsMobile-1.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/HackerNewsMobile-1.apk -------------------------------------------------------------------------------- /HNMobile/HackerNewsMobile-2.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/HackerNewsMobile-2.apk -------------------------------------------------------------------------------- /HNMobile/HackerNewsMobile-3.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/HackerNewsMobile-3.apk -------------------------------------------------------------------------------- /HNMobile/HackerNewsMobile-4.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/HackerNewsMobile-4.apk -------------------------------------------------------------------------------- /HNMobile/HackerNewsMobile-5.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/HackerNewsMobile-5.apk -------------------------------------------------------------------------------- /HNMobile/resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/splash.png -------------------------------------------------------------------------------- /HNMobile/my-release-key.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/my-release-key.keystore -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/www/lib/ionic/fonts/ionicons.eot -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/www/lib/ionic/fonts/ionicons.ttf -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/www/lib/ionic/fonts/ionicons.woff -------------------------------------------------------------------------------- /HNMobile/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HNMobile", 3 | "private": "true", 4 | "devDependencies": { 5 | "ionic": "driftyco/ionic-bower#1.0.0" 6 | } 7 | } -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /HNMobile/www/templates/playlist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Playlist

4 |
5 |
6 | -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "codename": "uranium-unicorn", 4 | "date": "2015-05-12", 5 | "time": "17:23:22" 6 | } 7 | -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /HNMobile/resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /HNMobile/resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /HNMobile/resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /HNMobile/resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /HNMobile/resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /HNMobile/.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | node_modules/ 5 | platforms/ 6 | plugins/ 7 | -------------------------------------------------------------------------------- /HNMobile/resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /HNMobile/resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /HNMobile/resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PebbleFrame/hacker-news-remake/HEAD/HNMobile/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | public/dist/ 4 | 5 | 6 | 7 | 8 | *.log 9 | 10 | build/ 11 | 12 | .idea 13 | .idea/ 14 | .iml 15 | #Floobits 16 | .floo 17 | .flooignore -------------------------------------------------------------------------------- /server/cache/cacheRoutes.js: -------------------------------------------------------------------------------- 1 | var cacheController = require('./cacheController'); 2 | 3 | module.exports = function (app, router) { 4 | 5 | router 6 | .get('/topStories', cacheController.topStories); 7 | 8 | }; -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_progress.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Progress 4 | * -------------------------------------------------- 5 | */ 6 | 7 | progress { 8 | display: block; 9 | margin: $progress-margin; 10 | width: $progress-width; 11 | } 12 | -------------------------------------------------------------------------------- /server/graph/graphRoutes.js: -------------------------------------------------------------------------------- 1 | var graphController = require('./graphController.js') 2 | var checkAuth = require('../utils/checkAuth.js'); 3 | 4 | module.exports = function (app, router) { 5 | //Router routing to the controller 6 | router 7 | .get('/fetch', graphController.fetch); 8 | } -------------------------------------------------------------------------------- /server/graph/graphModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | // DB to store graph data 4 | var GraphSchema = mongoose.Schema({ 5 | storyId: { 6 | type: String, 7 | required: true, 8 | unique: true 9 | }, 10 | graph: { 11 | type: Object, 12 | required: true 13 | } 14 | }); 15 | 16 | var Graph = mongoose.model('graphs', GraphSchema); 17 | 18 | module.exports = Graph; -------------------------------------------------------------------------------- /public/app/topStories/topStories.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
more
7 |
8 | 9 | -------------------------------------------------------------------------------- /server/users/userRoutes.js: -------------------------------------------------------------------------------- 1 | var userController = require('./userController.js') 2 | var checkAuth = require('../utils/checkAuth.js'); 3 | 4 | module.exports = function (app, router) { 5 | //Router routing to the controller 6 | userController.app = app; 7 | router 8 | .post('/signup', userController.signup) 9 | .post('/signin', userController.signin) 10 | .post('/updateFollowing', checkAuth, userController.updateFollowing) 11 | } -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_backdrop.scss: -------------------------------------------------------------------------------- 1 | 2 | .backdrop { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | z-index: $z-index-backdrop; 7 | 8 | width: 100%; 9 | height: 100%; 10 | 11 | background-color: $loading-backdrop-bg-color; 12 | 13 | visibility: hidden; 14 | opacity: 0; 15 | 16 | &.visible { 17 | visibility: visible; 18 | } 19 | &.active { 20 | opacity: 1; 21 | } 22 | 23 | @include transition($loading-backdrop-fadein-duration opacity linear); 24 | } 25 | -------------------------------------------------------------------------------- /public/app/currentlyFollowing/currentlyFollowing.html: -------------------------------------------------------------------------------- 1 |
2 |

Following

3 |
4 | 5 | 6 |
7 |
8 | {{user}} x 9 |
10 |
-------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": false, 4 | "eqeqeq": true, 5 | "es3": false, 6 | "forin": false, 7 | "freeze": false, 8 | "noempty": false, 9 | "nonew": true, 10 | "plusplus": false, 11 | "strict": false, 12 | "laxbreak": true, 13 | "multistr": true, 14 | "eqnull": true, 15 | "expr": true, 16 | "asi": true, 17 | "immed": false, 18 | "latedef": false, 19 | "newcap": false, 20 | "noarg": false, 21 | "trailing": false, 22 | "loopfunc": true, 23 | "smarttabs": true 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var app = require('./server/server.js'); 2 | var port = process.env.HACKFEED_PORT || 3000; 3 | 4 | app.listen(port); 5 | 6 | var http = require("http"); 7 | setInterval(function() { 8 | http.get("http://hnmobileapp.herokuapp.com"); 9 | }, 600000); // every 10 minutes (600000) 10 | 11 | var http = require("http"); 12 | setInterval(function() { 13 | http.get("http://goosewindmill.herokuapp.com"); 14 | }, 600000); // every 10 minutes (600000) 15 | 16 | console.log("Server running on port: " + port + "/\nCTRL + C to shutdown"); -------------------------------------------------------------------------------- /public/app/currentlyFollowing/currentlyFollowing.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.currentlyFollowing', []) 2 | 3 | .controller('CurrentlyFollowingController', function ($scope, Followers) { 4 | $scope.currentlyFollowing = Followers.following; 5 | 6 | $scope.unfollow = function(user){ 7 | Followers.removeFollower(user); 8 | $scope.currentlyFollowing = Followers.following; 9 | }; 10 | 11 | $scope.follow = function(user){ 12 | Followers.addFollower(user); 13 | $scope.newFollow = ""; 14 | $scope.currentlyFollowing = Followers.following; 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /HNMobile/www/templates/top-stories.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /HNMobile/www/templates/most-points.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /HNMobile/www/templates/most-recent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /HNMobile/www/templates/most-comments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 14 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /protractor_conf.js: -------------------------------------------------------------------------------- 1 | // An example configuration file. 2 | exports.config = { 3 | directConnect: true, 4 | 5 | // Capabilities to be passed to the webdriver instance. 6 | capabilities: { 7 | 'browserName': 'chrome' 8 | }, 9 | 10 | // Spec patterns are relative to the current working directly when 11 | // protractor is called. 12 | specs: ['test/e2e/*.js'], 13 | // chromeOnly: true, 14 | // chromeDriver: './node_modules/protractor/selenium/chromedriver', 15 | 16 | // Options to be passed to Jasmine-node. 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /public/app/personal/personal.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.personal', []) 2 | 3 | .controller('PersonalController', function ($scope, $window, Links, Dashboard, Graph) { 4 | $scope.stories = Links.personalStories; 5 | $scope.users = Dashboard.following; 6 | $scope.perPage = 30; 7 | $scope.index = $scope.perPage; 8 | 9 | var init = function(){ 10 | fetchUsers(); 11 | }; 12 | 13 | var fetchUsers = function(){ 14 | Links.getPersonalStories($scope.users); 15 | }; 16 | 17 | $scope.graphStory = function(storyId){ 18 | Graph.makeGraph(storyId); 19 | $scope.currentGraphedStory = storyId; 20 | }; 21 | 22 | init(); 23 | }); 24 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/ionicons/ionicons.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | @import "ionicons-variables"; 3 | /*! 4 | Ionicons, v2.0.1 5 | Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ 6 | https://twitter.com/benjsperry https://twitter.com/ionicframework 7 | MIT License: https://github.com/driftyco/ionicons 8 | 9 | Android-style icons originally built by Google’s 10 | Material Design Icons: https://github.com/google/material-design-icons 11 | used under CC BY http://creativecommons.org/licenses/by/4.0/ 12 | Modified icons to fit ionicon’s grid from original. 13 | */ 14 | 15 | @import "ionicons-font"; 16 | @import "ionicons-icons"; 17 | -------------------------------------------------------------------------------- /public/app/tabs/tabs.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | All 4 |
5 |
6 |
7 |
8 | 9 | Personal 10 |
11 |
12 |
13 |
14 |
-------------------------------------------------------------------------------- /server/utils/checkAuth.js: -------------------------------------------------------------------------------- 1 | var UserModel = require('../users/userModel'); 2 | var jwt = require('jwt-simple'); 3 | 4 | module.exports = function(req, res, next) { 5 | var token = (req.body && req.body.access_token) || (req.query && req.query.access_token) || req.headers['x-access-token']; 6 | 7 | if (token) { 8 | try { 9 | var decoded = jwt.decode(token, 'PROVOLONE'); 10 | 11 | UserModel.findOne({ username: decoded.user }, function(err, user) { 12 | req.user = user; 13 | next(); 14 | }); 15 | 16 | } catch (err) { 17 | console.log(err); 18 | return next(); 19 | } 20 | } else { 21 | next(); 22 | } 23 | }; -------------------------------------------------------------------------------- /HNMobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hnmobile", 3 | "version": "1.0.0", 4 | "description": "HNMobile: An Ionic project", 5 | "dependencies": { 6 | "gulp": "^3.5.6", 7 | "gulp-sass": "^1.3.3", 8 | "gulp-concat": "^2.2.0", 9 | "gulp-minify-css": "^0.3.0", 10 | "gulp-rename": "^1.2.0" 11 | }, 12 | "devDependencies": { 13 | "bower": "^1.3.3", 14 | "gulp-util": "^2.2.14", 15 | "shelljs": "^0.3.0" 16 | }, 17 | "cordovaPlugins": [ 18 | "org.apache.cordova.device", 19 | "org.apache.cordova.console", 20 | "com.ionic.keyboard", 21 | "org.apache.cordova.inappbrowser" 22 | ], 23 | "cordovaPlatforms": [ 24 | "android" 25 | ] 26 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Goose-Windmill", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/Goose-Windmill/Goose-Windmill", 5 | "authors": [ 6 | "Siddharth Sukumar " 7 | ], 8 | "description": "\"Hacking hacker news\"", 9 | "moduleType": [ 10 | "globals", 11 | "node" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "angular": "~1.3.15", 23 | "underscore": "~1.8.3", 24 | "angular-mocks": "~1.3.15", 25 | "angular-route": "~1.3.15", 26 | "angular-jwt": "~0.0.7", 27 | "angular-ui-router": "~0.2.14" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var mongoose = require('mongoose'); 3 | 4 | var app = express(); 5 | 6 | // ------ Connection to mongoose database ----- 7 | var uristring = 8 | process.env.MONGOLAB_URI || 9 | process.env.MONGOHQ_URL || 10 | 'mongodb://localhost/hfPersonal'; 11 | 12 | mongoose.connect(uristring, {}, function (err, res) { 13 | if (err) { 14 | console.log ('ERROR connecting to: ' + uristring + '. ' + err); 15 | } else { 16 | console.log ('Succeeded connected to: ' + uristring); 17 | } 18 | }); 19 | 20 | require('./config/middleware.js')(app, express); 21 | 22 | // app.listen(3000, function(){ 23 | // console.log("Listening on 3000"); 24 | // }); 25 | 26 | module.exports = app; -------------------------------------------------------------------------------- /test-main.js: -------------------------------------------------------------------------------- 1 | var allTestFiles = []; 2 | var TEST_REGEXP = /(spec|test)\.js$/i; 3 | 4 | var pathToModule = function(path) { 5 | return path.replace(/^\/base\//, '').replace(/\.js$/, ''); 6 | }; 7 | 8 | Object.keys(window.__karma__.files).forEach(function(file) { 9 | if (TEST_REGEXP.test(file)) { 10 | // Normalize paths to RequireJS module names. 11 | allTestFiles.push(pathToModule(file)); 12 | } 13 | }); 14 | 15 | require.config({ 16 | // Karma serves files under /base, which is the basePath from your config file 17 | baseUrl: '/base', 18 | 19 | // dynamically load all test files 20 | deps: allTestFiles, 21 | 22 | // we have to kickoff jasmine, as it is asynchronous 23 | callback: window.__karma__.start 24 | }); 25 | -------------------------------------------------------------------------------- /public/app/tabs/tabs.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.tabs', []) 2 | 3 | .controller('TabsController', function ($scope, $window, Links, Dashboard) { 4 | // If a user refreshes when the location is '/personal', 5 | // it will stay on '/personal'. 6 | var hash = $window.location.hash.split('/')[1]; 7 | hash = !hash ? 'all' : hash; 8 | $scope.currentTab = hash; 9 | 10 | // What is angle? Don't worry. This just makes the 11 | // refresh button do a cool spin animation. We splurged. 12 | $scope.angle = 360; 13 | 14 | $scope.changeTab = function(newTab){ 15 | $scope.currentTab = newTab; 16 | }; 17 | 18 | $scope.refreshs = function(){ 19 | console.log('hereeeee'); 20 | Links.getTopStories(); 21 | Links.getPersonalStories(Dashboard.following); 22 | $scope.angle += 360; 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /server/cache/cacheController.js: -------------------------------------------------------------------------------- 1 | var Cache = require('./cacheModel.js'); 2 | 3 | var defaultCorsHeaders = { 4 | "access-control-allow-origin": "*", 5 | "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", 6 | "access-control-allow-headers": "content-type, accept", 7 | "access-control-max-age": 10 // Seconds. 8 | }; 9 | 10 | module.exports = { 11 | topStories: function(request, response) { 12 | Cache.getTopStories(function(err,results){ 13 | if(!err){ 14 | response.set(defaultCorsHeaders); 15 | response.json(results); 16 | }else{ 17 | response.status(500).send(err); 18 | } 19 | }); 20 | } 21 | }; 22 | 23 | // Initialize and refresh the top story data every two minutes 24 | Cache.updateTopStories(); 25 | setInterval(Cache.updateTopStories, 120000); -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/ionic.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @import 4 | // Ionicons 5 | "ionicons/ionicons.scss", 6 | 7 | // Variables 8 | "mixins", 9 | "variables", 10 | 11 | // Base 12 | "reset", 13 | "scaffolding", 14 | "type", 15 | 16 | // Components 17 | "action-sheet", 18 | "backdrop", 19 | "bar", 20 | "tabs", 21 | "menu", 22 | "modal", 23 | "popover", 24 | "popup", 25 | "loading", 26 | "items", 27 | "list", 28 | "badge", 29 | "slide-box", 30 | "refresher", 31 | "spinner", 32 | 33 | // Forms 34 | "form", 35 | "checkbox", 36 | "toggle", 37 | "radio", 38 | "range", 39 | "select", 40 | "progress", 41 | 42 | // Buttons 43 | "button", 44 | "button-bar", 45 | 46 | // Util 47 | "grid", 48 | "util", 49 | "platform", 50 | 51 | // Animations 52 | "animations", 53 | "transitions"; 54 | -------------------------------------------------------------------------------- /HNMobile/www/templates/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | This app is a project by several students at Hack Reactor. It is by no means perfect, so suggestions for improvement are definitely welcome! 8 |

9 | The "score" is based on the official Hacker News ranking algorithm, but I wasn't able to figure out how to implement it exactly, so right now it's just a loose approximation. 10 |

11 | The original developers of the web version of this app are Kenny Tran, Matthew Rourke, Jennifer Bland, and Sid Sukumar. I, Michael Cheng, designed this mobile version for a legacy project, working in a group with Christina Holland, Jeff Peoples, and Vinaya Gopisetti. 12 | 13 |
14 |
15 |
16 | 17 |
18 |
-------------------------------------------------------------------------------- /HNMobile/scss/ionic.app.scss: -------------------------------------------------------------------------------- 1 | /* 2 | To customize the look and feel of Ionic, you can override the variables 3 | in ionic's _variables.scss file. 4 | 5 | For example, you might change some of the default colors: 6 | 7 | $light: #fff !default; 8 | $stable: #f8f8f8 !default; 9 | $positive: #387ef5 !default; 10 | $calm: #11c1f3 !default; 11 | $balanced: #33cd5f !default; 12 | $energized: #ffc900 !default; 13 | $assertive: #ef473a !default; 14 | $royal: #886aea !default; 15 | $dark: #444 !default; 16 | */ 17 | 18 | // The path for our ionicons font files, relative to the built CSS in www/css 19 | $ionicons-font-path: "../lib/ionic/fonts" !default; 20 | 21 | // Include all of Ionic 22 | @import "www/lib/ionic/scss/ionic"; 23 | 24 | -------------------------------------------------------------------------------- /public/app/topStories/topStories.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.topStories', []) 2 | 3 | .controller('TopStoriesController', function ($scope, $window, Links, Dashboard, ezfb, Graph) { 4 | angular.extend($scope, Links); 5 | $scope.stories = Links.topStories; 6 | console.log($scope.stories); 7 | $scope.perPage = 30; 8 | $scope.index = $scope.perPage; 9 | $scope.currentGraphedStory; 10 | 11 | $scope.shareStory = function(url){ 12 | ezfb.ui({ 13 | method: 'share', 14 | href: url 15 | }, function(response){}); 16 | }; 17 | 18 | $scope.graphStory = function(storyId){ 19 | Graph.makeGraph(storyId); 20 | $scope.currentGraphedStory = storyId; 21 | }; 22 | 23 | $scope.currentlyFollowing = Dashboard.following; 24 | 25 | $scope.getData = function() { 26 | Links.getTopStories(); 27 | }; 28 | 29 | $scope.addUser = function(username) { 30 | Dashboard.addFollower(username); 31 | }; 32 | 33 | $scope.getData(); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /public/app/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |

Dashboard

3 |
Save Feed
4 |
5 |

Saved Feeds

6 |
{{feed}}
7 |
8 |
9 |

Following

10 |
{{user}} 11 |
12 |
13 |
14 |

Hashes:

15 |
{{hash}} 16 |
17 |
18 |
Delete Selected
19 |
-------------------------------------------------------------------------------- /server/config/middleware.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | 3 | module.exports = function(app, express){ 4 | // secret string for token 5 | app.set('jwtTokenSecret', 'PROVOLONE'); 6 | 7 | //Static file locations 8 | app.use(express.static(__dirname + '/../../public')); 9 | app.use(express.static(__dirname + '/../../bower_components')); 10 | 11 | //Server App middleware 12 | app.use(bodyParser.json()); 13 | app.use(bodyParser.urlencoded({extended: false})); 14 | 15 | //Establish routers and inject the router into the routes file 16 | var userRouter = express.Router(); 17 | require('../users/userRoutes.js')(app, userRouter); 18 | 19 | var cacheRouter = express.Router(); 20 | require('../cache/cacheRoutes.js')(app, cacheRouter); 21 | 22 | var graphRouter = express.Router(); 23 | require('../graph/graphRoutes.js')(app, graphRouter); 24 | 25 | //Establish routes 26 | app.use('/api/users', userRouter); 27 | app.use('/api/cache', cacheRouter); 28 | app.use('/api/graph', graphRouter); 29 | }; 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/app/personal/personal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | {{story.author}} 12 | {{story.created_at | fromNow}} | 13 | parent | 14 | on: {{story.story_title}} 15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
more
23 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_loading.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Loading 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .loading-container { 8 | position: absolute; 9 | left: 0; 10 | top: 0; 11 | right: 0; 12 | bottom: 0; 13 | 14 | z-index: $z-index-loading; 15 | 16 | @include display-flex(); 17 | @include justify-content(center); 18 | @include align-items(center); 19 | 20 | @include transition(0.2s opacity linear); 21 | visibility: hidden; 22 | opacity: 0; 23 | 24 | &:not(.visible) .icon { 25 | display: none; 26 | } 27 | &.visible { 28 | visibility: visible; 29 | } 30 | &.active { 31 | opacity: 1; 32 | } 33 | 34 | .loading { 35 | padding: $loading-padding; 36 | 37 | border-radius: $loading-border-radius; 38 | background-color: $loading-bg-color; 39 | 40 | color: $loading-text-color; 41 | 42 | text-align: center; 43 | text-overflow: ellipsis; 44 | font-size: $loading-font-size; 45 | 46 | h1, h2, h3, h4, h5, h6 { 47 | color: $loading-text-color; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_slide-box.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Slide Box 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .slider { 8 | position: relative; 9 | visibility: hidden; 10 | // Make sure items don't scroll over ever 11 | overflow: hidden; 12 | } 13 | 14 | .slider-slides { 15 | position: relative; 16 | height: 100%; 17 | } 18 | 19 | .slider-slide { 20 | position: relative; 21 | display: block; 22 | float: left; 23 | width: 100%; 24 | height: 100%; 25 | vertical-align: top; 26 | } 27 | 28 | .slider-slide-image { 29 | > img { 30 | width: 100%; 31 | } 32 | } 33 | 34 | .slider-pager { 35 | position: absolute; 36 | bottom: 20px; 37 | z-index: $z-index-slider-pager; 38 | width: 100%; 39 | height: 15px; 40 | text-align: center; 41 | 42 | .slider-pager-page { 43 | display: inline-block; 44 | margin: 0px 3px; 45 | width: 15px; 46 | color: #000; 47 | text-decoration: none; 48 | 49 | opacity: 0.3; 50 | 51 | &.active { 52 | @include transition(opacity 0.4s ease-in); 53 | opacity: 1; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/graph/graphController.js: -------------------------------------------------------------------------------- 1 | var Graph = require('./graphModel.js'); 2 | var request = require('request'); 3 | 4 | var GraphController = {}; 5 | 6 | //Set headers 7 | var headers = { 8 | 'User-Agent': 'Hacker Feed', 9 | 'Content-Type': 'application/json' 10 | }; 11 | 12 | // fetch graph data for a user via API calls and put it in DB 13 | // TODO: store story data in DB and fetch from DB if it hasn't changed 14 | // (instead of sending an API request) 15 | GraphController.fetch = function (req, res, next) { 16 | console.log(req.query); 17 | var storyId = req.query.storyId || '9546311'; 18 | var queryUrl = 'http://hn.algolia.com/api/v1/items/' + storyId; 19 | //var queryUrl = 'https://hacker-news.firebaseio.com/v0/item/' + storyId + '.json?print=pretty'; 20 | var options = { 21 | url: queryUrl, 22 | method: 'GET', 23 | headers: headers 24 | }; 25 | 26 | // Perform the firebase API request 27 | request(options, function(error, response, html){ 28 | var data = JSON.parse(response.body); 29 | res.status(200).send(data); 30 | }); 31 | 32 | }; 33 | 34 | module.exports = GraphController; -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/ionicons/_ionicons-font.scss: -------------------------------------------------------------------------------- 1 | // Ionicons Font Path 2 | // -------------------------- 3 | 4 | @font-face { 5 | font-family: $ionicons-font-family; 6 | src:url("#{$ionicons-font-path}/ionicons.eot?v=#{$ionicons-version}"); 7 | src:url("#{$ionicons-font-path}/ionicons.eot?v=#{$ionicons-version}#iefix") format("embedded-opentype"), 8 | url("#{$ionicons-font-path}/ionicons.ttf?v=#{$ionicons-version}") format("truetype"), 9 | url("#{$ionicons-font-path}/ionicons.woff?v=#{$ionicons-version}") format("woff"), 10 | url("#{$ionicons-font-path}/ionicons.woff") format("woff"), /* for WP8 */ 11 | url("#{$ionicons-font-path}/ionicons.svg?v=#{$ionicons-version}#Ionicons") format("svg"); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | .ion { 17 | display: inline-block; 18 | font-family: $ionicons-font-family; 19 | speak: none; 20 | font-style: normal; 21 | font-weight: normal; 22 | font-variant: normal; 23 | text-transform: none; 24 | text-rendering: auto; 25 | line-height: 1; 26 | -webkit-font-smoothing: antialiased; 27 | -moz-osx-font-smoothing: grayscale; 28 | } 29 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_button-bar.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Button Bar 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .button-bar { 8 | @include display-flex(); 9 | @include flex(1); 10 | width: 100%; 11 | 12 | &.button-bar-inline { 13 | display: block; 14 | width: auto; 15 | 16 | @include clearfix(); 17 | 18 | > .button { 19 | width: auto; 20 | display: inline-block; 21 | float: left; 22 | } 23 | } 24 | } 25 | 26 | .button-bar > .button { 27 | @include flex(1); 28 | display: block; 29 | 30 | overflow: hidden; 31 | 32 | padding: 0 16px; 33 | 34 | width: 0; 35 | 36 | border-width: 1px 0px 1px 1px; 37 | border-radius: 0; 38 | text-align: center; 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | 42 | &:before, 43 | .icon:before { 44 | line-height: 44px; 45 | } 46 | 47 | &:first-child { 48 | border-radius: $button-border-radius 0px 0px $button-border-radius; 49 | } 50 | &:last-child { 51 | border-right-width: 1px; 52 | border-radius: 0px $button-border-radius $button-border-radius 0px; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /HNMobile/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/app/services/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.authService', []) 2 | 3 | .factory('Auth', function ($http, $location, $window) { 4 | var username = null; 5 | var getUser = function(){ 6 | return username; 7 | } 8 | var signin = function (user) { 9 | username = user.username; 10 | return $http({ 11 | method: 'POST', 12 | url: '/api/users/signin', 13 | data: user 14 | }) 15 | .then(function (resp) { 16 | return resp.data; 17 | }); 18 | }; 19 | 20 | var signup = function (user) { 21 | username = user.username; 22 | return $http({ 23 | method: 'POST', 24 | url: '/api/users/signup', 25 | data: user 26 | }) 27 | .then(function (resp) { 28 | return resp.data; 29 | }); 30 | }; 31 | 32 | var isAuth = function () { 33 | return !!$window.localStorage.getItem('com.hack'); 34 | }; 35 | 36 | var signout = function () { 37 | $window.localStorage.removeItem('com.hack'); 38 | $window.localStorage.removeItem('hfUsers'); 39 | $window.localStorage.removeItem('hfUser'); 40 | }; 41 | 42 | 43 | return { 44 | signin: signin, 45 | signup: signup, 46 | isAuth: isAuth, 47 | signout: signout, 48 | getUser: getUser 49 | }; 50 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Ready](https://badge.waffle.io/goose-windmill/goose-windmill.png?label=ready&title=Ready)](https://waffle.io/goose-windmill/goose-windmill) 2 | # Goose-Windmill 3 | 4 | > Improve Hacker News UX by automating a stream of user specified content. 5 | 6 | ## Team 7 | 8 | - __Product Owner__: Kenny Tran 9 | - __Scrum Master__: Matthew Rourke 10 | - __Development Team Members__: Jennifer Bland, Siddharth Sukumar 11 | 12 | ## Table of Contents 13 | 14 | 1. [Usage](#Usage) 15 | 1. [Requirements](#requirements) 16 | 1. [Development](#development) 17 | 1. [Installing Dependencies](#installing-dependencies) 18 | 1. [Tasks](#tasks) 19 | 1. [Team](#team) 20 | 1. [Contributing](#contributing) 21 | 22 | ## Usage 23 | 24 | > Some usage instructions 25 | 26 | ## Requirements 27 | 28 | - Node 29 | - Angular 30 | - Express 31 | - MongoDB 32 | 33 | ## Development 34 | 35 | ### Installing Dependencies 36 | 37 | From within the root directory: 38 | 39 | ```sh 40 | sudo npm install -g bower 41 | npm install 42 | bower install 43 | ``` 44 | 45 | ### Roadmap 46 | 47 | View the project roadmap [here](LINK_TO_PROJECT_ISSUES) 48 | 49 | 50 | ## Contributing 51 | 52 | See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. 53 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_menu.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Menus 4 | * -------------------------------------------------- 5 | * Side panel structure 6 | */ 7 | 8 | .menu { 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | z-index: $z-index-menu; 13 | overflow: hidden; 14 | 15 | min-height: 100%; 16 | max-height: 100%; 17 | width: $menu-width; 18 | 19 | background-color: $menu-bg; 20 | 21 | .scroll-content { 22 | z-index: $z-index-menu-scroll-content; 23 | } 24 | 25 | .bar-header { 26 | z-index: $z-index-menu-bar-header; 27 | } 28 | } 29 | 30 | .menu-content { 31 | @include transform(none); 32 | box-shadow: $menu-side-shadow; 33 | } 34 | 35 | .menu-open .menu-content .pane, 36 | .menu-open .menu-content .scroll-content { 37 | pointer-events: none; 38 | } 39 | 40 | .grade-b .menu-content, 41 | .grade-c .menu-content { 42 | @include box-sizing(content-box); 43 | right: -1px; 44 | left: -1px; 45 | border-right: 1px solid #ccc; 46 | border-left: 1px solid #ccc; 47 | box-shadow: none; 48 | } 49 | 50 | .menu-left { 51 | left: 0; 52 | } 53 | 54 | .menu-right { 55 | right: 0; 56 | } 57 | 58 | .aside-open.aside-resizing .menu-right { 59 | display: none; 60 | } 61 | 62 | .menu-animated { 63 | @include transition-transform($menu-animation-speed ease); 64 | } 65 | -------------------------------------------------------------------------------- /public/app/partials/story.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | {{story.title}} 7 | 8 | {{story.url.split('/')[2]}} 9 |
10 | {{story.points}} points 11 | 12 | 13 | 14 | 15 | {{ story.author}} 16 | {{story.created_at | fromNow}} 17 | 18 | {{story.num_comments ? story.num_comments + ' comments' : 'discuss'}} 19 | 20 | 21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /HNMobile/www/templates/comments.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Comments for: 5 |
{{comments[0].story_title}}
6 | 7 | 8 | {{comments[0].story_url.split('/')[2]}} 9 |
10 | 11 | 12 | Comments page 13 | 14 |
15 | 16 | 17 |
18 |
{{comment.author}} commented {{comment.created_at | fromNow}}
19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | 29 | 30 |
31 |
-------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_animations.scss: -------------------------------------------------------------------------------- 1 | 2 | // Slide up from the bottom, used for modals 3 | // ------------------------------- 4 | 5 | .slide-in-up { 6 | @include translate3d(0, 100%, 0); 7 | } 8 | .slide-in-up.ng-enter, 9 | .slide-in-up > .ng-enter { 10 | @include transition(all cubic-bezier(.1, .7, .1, 1) 400ms); 11 | } 12 | .slide-in-up.ng-enter-active, 13 | .slide-in-up > .ng-enter-active { 14 | @include translate3d(0, 0, 0); 15 | } 16 | 17 | .slide-in-up.ng-leave, 18 | .slide-in-up > .ng-leave { 19 | @include transition(all ease-in-out 250ms); 20 | } 21 | 22 | 23 | // Scale Out 24 | // Scale from hero (1 in this case) to zero 25 | // ------------------------------- 26 | 27 | @-webkit-keyframes scaleOut { 28 | from { -webkit-transform: scale(1); opacity: 1; } 29 | to { -webkit-transform: scale(0.8); opacity: 0; } 30 | } 31 | @keyframes scaleOut { 32 | from { transform: scale(1); opacity: 1; } 33 | to { transform: scale(0.8); opacity: 0; } 34 | } 35 | 36 | 37 | // Super Scale In 38 | // Scale from super (1.x) to duper (1 in this case) 39 | // ------------------------------- 40 | 41 | @-webkit-keyframes superScaleIn { 42 | from { -webkit-transform: scale(1.2); opacity: 0; } 43 | to { -webkit-transform: scale(1); opacity: 1 } 44 | } 45 | @keyframes superScaleIn { 46 | from { transform: scale(1.2); opacity: 0; } 47 | to { transform: scale(1); opacity: 1; } 48 | } 49 | -------------------------------------------------------------------------------- /public/lib/angular-jwt.min.js: -------------------------------------------------------------------------------- 1 | !function(){angular.module("angular-jwt",["angular-jwt.interceptor","angular-jwt.jwt"]),angular.module("angular-jwt.interceptor",[]).provider("jwtInterceptor",function(){this.authHeader="Authorization",this.authPrefix="Bearer ",this.tokenGetter=function(){return null};var e=this;this.$get=["$q","$injector","$rootScope",function(t,r,n){return{request:function(n){if(n.skipAuthorization)return n;if(n.headers=n.headers||{},n.headers[e.authHeader])return n;var a=t.when(r.invoke(e.tokenGetter,this,{config:n}));return a.then(function(t){return t&&(n.headers[e.authHeader]=e.authPrefix+t),n})},responseError:function(e){return 401===e.status&&n.$broadcast("unauthenticated",e),t.reject(e)}}}]}),angular.module("angular-jwt.jwt",[]).service("jwtHelper",function(){this.urlBase64Decode=function(e){var t=e.replace("-","+").replace("_","/");switch(t.length%4){case 0:break;case 2:t+="==";break;case 3:t+="=";break;default:throw"Illegal base64url string!"}return decodeURIComponent(escape(window.atob(t)))},this.decodeToken=function(e){var t=e.split(".");if(3!==t.length)throw new Error("JWT must have 3 parts");var r=this.urlBase64Decode(t[1]);if(!r)throw new Error("Cannot decode the token");return JSON.parse(r)},this.getTokenExpirationDate=function(e){var t;if(t=this.decodeToken(e),!t.exp)return null;var r=new Date(0);return r.setUTCSeconds(t.exp),r},this.isTokenExpired=function(e){var t=this.getTokenExpirationDate(e);return t?!(t.valueOf()>(new Date).valueOf()):!1}})}(); -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_radio.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Radio Button Inputs 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .item-radio { 8 | padding: 0; 9 | 10 | &:hover { 11 | cursor: pointer; 12 | } 13 | } 14 | 15 | .item-radio .item-content { 16 | /* give some room to the right for the checkmark icon */ 17 | padding-right: $item-padding * 4; 18 | } 19 | 20 | .item-radio .radio-icon { 21 | /* checkmark icon will be hidden by default */ 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | z-index: $z-index-item-radio; 26 | visibility: hidden; 27 | padding: $item-padding - 2; 28 | height: 100%; 29 | font-size: 24px; 30 | } 31 | 32 | .item-radio input { 33 | /* hide any radio button inputs elements (the ugly circles) */ 34 | position: absolute; 35 | left: -9999px; 36 | 37 | &:checked ~ .item-content { 38 | /* style the item content when its checked */ 39 | background: #f7f7f7; 40 | } 41 | 42 | &:checked ~ .radio-icon { 43 | /* show the checkmark icon when its checked */ 44 | visibility: visible; 45 | } 46 | } 47 | 48 | // Hack for Android to correctly display the checked item 49 | // http://timpietrusky.com/advanced-checkbox-hack 50 | .platform-android.grade-b .item-radio, 51 | .platform-android.grade-c .item-radio { 52 | -webkit-animation: androidCheckedbugfix infinite 1s; 53 | } 54 | @-webkit-keyframes androidCheckedbugfix { 55 | from { padding: 0; } 56 | to { padding: 0; } 57 | } 58 | -------------------------------------------------------------------------------- /public/app/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.userDashboard', []) 2 | 3 | .controller('DashboardController', function ($scope, Dashboard) { 4 | angular.extend($scope,Dashboard); 5 | $scope.deleteSelected(); 6 | }) 7 | 8 | .controller('FollowingController', function ($scope,Dashboard) { 9 | angular.extend($scope,Dashboard); 10 | console.log("HEllo") 11 | $scope.userSelected = false; 12 | $scope.toggleUser = function(user){ 13 | console.log("Toggling") 14 | if($scope.userSelected ===false){ 15 | $scope.userSelected = true; 16 | $scope.addSelectedUser(user); 17 | } 18 | else{ 19 | $scope.userSelected = false; 20 | $scope.removeSelectedUser(user); 21 | } 22 | } 23 | }) 24 | .controller('HashController', function ($scope,Dashboard) { 25 | angular.extend($scope,Dashboard); 26 | $scope.hashSelected = false; 27 | $scope.toggleHash = function(hash){ 28 | if($scope.hashSelected ===false){ 29 | $scope.hashSelected = true; 30 | $scope.addSelectedHash(hash); 31 | } 32 | else{ 33 | $scope.userSelected = false; 34 | $scope.removeSelectedHash(user); 35 | } 36 | } 37 | }) 38 | .controller('FeedController', function ($scope,Dashboard) { 39 | angular.extend($scope,Dashboard); 40 | $scope.hashSelected = false; 41 | $scope.selectFeed = function(feed){ 42 | if($scope.feedSelected ===false){ 43 | $scope.feedSelected = true; 44 | $scope.setSelectedFeed(feed); 45 | } 46 | else{ 47 | $scope.feedSelected = false; 48 | $scope.removeSelectedHash(user); 49 | } 50 | } 51 | }); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hack Feed 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /HNMobile/www/templates/menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

Menu

18 |
19 | 20 | 21 | 22 | Top Stories 23 | 24 | 25 | Most Points 26 | 27 | 28 | Most Comments 29 | 30 | 31 | Most Recent 32 | 33 | 34 | Subscriptions 35 | 36 | 37 | Login 38 | 39 | 40 | About 41 | 42 | 43 | 44 |
45 |
-------------------------------------------------------------------------------- /HNMobile/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var bower = require('bower'); 4 | var concat = require('gulp-concat'); 5 | var sass = require('gulp-sass'); 6 | var minifyCss = require('gulp-minify-css'); 7 | var rename = require('gulp-rename'); 8 | var sh = require('shelljs'); 9 | 10 | var paths = { 11 | sass: ['./scss/**/*.scss'] 12 | }; 13 | 14 | gulp.task('default', ['sass']); 15 | 16 | gulp.task('sass', function(done) { 17 | gulp.src('./scss/ionic.app.scss') 18 | .pipe(sass({ 19 | errLogToConsole: true 20 | })) 21 | .pipe(gulp.dest('./www/css/')) 22 | .pipe(minifyCss({ 23 | keepSpecialComments: 0 24 | })) 25 | .pipe(rename({ extname: '.min.css' })) 26 | .pipe(gulp.dest('./www/css/')) 27 | .on('end', done); 28 | }); 29 | 30 | gulp.task('watch', function() { 31 | gulp.watch(paths.sass, ['sass']); 32 | }); 33 | 34 | gulp.task('install', ['git-check'], function() { 35 | return bower.commands.install() 36 | .on('log', function(data) { 37 | gutil.log('bower', gutil.colors.cyan(data.id), data.message); 38 | }); 39 | }); 40 | 41 | gulp.task('git-check', function(done) { 42 | if (!sh.which('git')) { 43 | console.log( 44 | ' ' + gutil.colors.red('Git is not installed.'), 45 | '\n Git, the version control system, is required to download Ionic.', 46 | '\n Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.', 47 | '\n Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.' 48 | ); 49 | process.exit(1); 50 | } 51 | done(); 52 | }); 53 | -------------------------------------------------------------------------------- /test/e2e/generalSpec.js: -------------------------------------------------------------------------------- 1 | describe('homepage', function() { 2 | var EC = protractor.ExpectedConditions; 3 | 4 | beforeEach(function() { 5 | browser.get('http://localhost:3000'); 6 | }); 7 | 8 | it('should load the page', function() { 9 | expect(element(by.css('.feed')).isPresent()).toBe(true); 10 | }); 11 | 12 | describe('following tab', function() { 13 | 14 | var input = element(by.model('newFollow')); 15 | var button = element(by.buttonText('Follow')); 16 | 17 | it('should add a typed-in user to follow', function() { 18 | input.sendKeys('pg'); 19 | button.click(); 20 | 21 | expect(element(by.repeater('user in currentlyFollowing').row(0)).isPresent()).toBe(true); 22 | expect(element(by.css('.followed-user')).getText()).toEqual('pg x'); 23 | }); 24 | 25 | it('should remove a followed user when clicked', function() { 26 | 27 | var x = $('.followed-user span'); 28 | x.click(); 29 | 30 | expect(element(by.repeater('user in currentlyFollowing').row(0)).isPresent()).toBe(false); 31 | }); 32 | 33 | it('should add a user to follow when their Follow button in topStories is clicked', function() { 34 | 35 | var topStory = element(by.repeater('story in stories').row(0)); 36 | var folButton = topStory.element(by.css('.bottom-row button')); 37 | var user; 38 | topStory.element(by.css('.bottom-row a.ng-binding')).getText().then(function (text) { 39 | folButton.click(); 40 | user = text; 41 | expect(element(by.css('.followed-user')).getText()).toEqual(user + ' x'); 42 | }); 43 | 44 | }); 45 | 46 | }); 47 | }); -------------------------------------------------------------------------------- /HNMobile/www/templates/personal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Following:

5 |
6 | 7 | {{user}} x 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | {{story.author}} commented on {{story.story_title}} {{story.created_at | fromNow }}: 24 |
25 |
26 |
27 | Add a comment 28 |
29 | 30 |
31 |
32 | 33 | 36 | 37 |
38 |
-------------------------------------------------------------------------------- /test/clientSpec/serviceSpec.js: -------------------------------------------------------------------------------- 1 | describe('followService tests', function() { 2 | 3 | beforeEach(module('hack')); 4 | 5 | beforeEach(inject(function(_Followers_) { 6 | Followers = _Followers_; 7 | })); 8 | 9 | describe('followService methods', function() { 10 | it ('should have a removeFollower method', function () { 11 | expect(Followers.removeFollower).to.be.a('function'); 12 | }); 13 | it ('should have an addFollower method', function () { 14 | expect(Followers.addFollower).to.be.a('function'); 15 | }); 16 | it ('should have an localToArr method', function () { 17 | expect(Followers.localToArr).to.be.a('function'); 18 | }); 19 | }); 20 | 21 | describe('addFollower method', function () { 22 | beforeEach(function() { 23 | $window.localStorage.setItem('hfUsers', 'user1,user2'); 24 | Followers.init(); 25 | }); 26 | afterEach(function() { 27 | $window.localStorage.removeItem('hfUsers'); 28 | }); 29 | it ('should add a follower to local storage', function () { 30 | Followers.addFollower('pinky'); 31 | expect($window.localStorage.getItem('hfUsers')).to.equal('user1,user2,pinky'); 32 | }); 33 | }); 34 | 35 | describe('removeFollower method', function () { 36 | beforeEach(function() { 37 | $window.localStorage.setItem('hfUsers', 'user1,pinky,user2'); 38 | Followers.init(); 39 | }); 40 | afterEach(function() { 41 | $window.localStorage.removeItem('hfUsers'); 42 | }); 43 | it ('should remove a follower from local storage', function () { 44 | Followers.removeFollower('pinky'); 45 | expect($window.localStorage.getItem('hfUsers')).to.equal('user1,user2'); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_badge.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Badges 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .badge { 8 | @include badge-style($badge-default-bg, $badge-default-text); 9 | z-index: $z-index-badge; 10 | display: inline-block; 11 | padding: 3px 8px; 12 | min-width: 10px; 13 | border-radius: $badge-border-radius; 14 | vertical-align: baseline; 15 | text-align: center; 16 | white-space: nowrap; 17 | font-weight: $badge-font-weight; 18 | font-size: $badge-font-size; 19 | line-height: $badge-line-height; 20 | 21 | &:empty { 22 | display: none; 23 | } 24 | } 25 | 26 | //Be sure to override specificity of rule that 'badge color matches tab color by default' 27 | .tabs .tab-item .badge, 28 | .badge { 29 | &.badge-light { 30 | @include badge-style($badge-light-bg, $badge-light-text); 31 | } 32 | &.badge-stable { 33 | @include badge-style($badge-stable-bg, $badge-stable-text); 34 | } 35 | &.badge-positive { 36 | @include badge-style($badge-positive-bg, $badge-positive-text); 37 | } 38 | &.badge-calm { 39 | @include badge-style($badge-calm-bg, $badge-calm-text); 40 | } 41 | &.badge-assertive { 42 | @include badge-style($badge-assertive-bg, $badge-assertive-text); 43 | } 44 | &.badge-balanced { 45 | @include badge-style($badge-balanced-bg, $badge-balanced-text); 46 | } 47 | &.badge-energized { 48 | @include badge-style($badge-energized-bg, $badge-energized-text); 49 | } 50 | &.badge-royal { 51 | @include badge-style($badge-royal-bg, $badge-royal-text); 52 | } 53 | &.badge-dark { 54 | @include badge-style($badge-dark-bg, $badge-dark-text); 55 | } 56 | } 57 | 58 | // Quick fix for labels/badges in buttons 59 | .button .badge { 60 | position: relative; 61 | top: -1px; 62 | } 63 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_platform.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Platform 4 | * -------------------------------------------------- 5 | * Platform specific tweaks 6 | */ 7 | 8 | .platform-ios.platform-cordova { 9 | // iOS has a status bar which sits on top of the header. 10 | // Bump down everything to make room for it. However, if 11 | // if its in Cordova, and set to fullscreen, then disregard the bump. 12 | &:not(.fullscreen) { 13 | .bar-header:not(.bar-subheader) { 14 | height: $bar-height + $ios-statusbar-height; 15 | 16 | &.item-input-inset .item-input-wrapper { 17 | margin-top: 19px !important; 18 | } 19 | 20 | > * { 21 | margin-top: $ios-statusbar-height; 22 | } 23 | } 24 | .tabs-top > .tabs, 25 | .tabs.tabs-top { 26 | top: $bar-height + $ios-statusbar-height; 27 | } 28 | 29 | .has-header, 30 | .bar-subheader { 31 | top: $bar-height + $ios-statusbar-height; 32 | } 33 | .has-subheader { 34 | top: $bar-height + $bar-subheader-height + $ios-statusbar-height; 35 | } 36 | .has-header.has-tabs-top { 37 | top: $bar-height + $tabs-height + $ios-statusbar-height; 38 | } 39 | .has-header.has-subheader.has-tabs-top { 40 | top: $bar-height + $bar-subheader-height + $tabs-height + $ios-statusbar-height; 41 | } 42 | } 43 | &.status-bar-hide { 44 | // Cordova doesn't adjust the body height correctly, this makes up for it 45 | margin-bottom: 20px; 46 | } 47 | } 48 | 49 | @media (orientation:landscape) { 50 | .platform-ios.platform-browser.platform-ipad { 51 | position: fixed; // required for iPad 7 Safari 52 | } 53 | } 54 | 55 | .platform-c:not(.enable-transitions) * { 56 | // disable transitions on grade-c devices (Android 2) 57 | -webkit-transition: none !important; 58 | transition: none !important; 59 | } 60 | -------------------------------------------------------------------------------- /HNMobile/www/js/partials/story.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | {{story.title}} 4 |

5 | 6 |
{{story.url.split('/')[2]}}
7 | 8 |
9 | 10 |
11 | {{story.points}} points | {{story.created_at | fromNow}} → Score: {{(story.created_at | fromNow).split(' ')[1] === 'hours' ? Math.round(((story.points - 1) / Math.pow(((story.created_at | fromNow).split(' ')[0] + 2), 1.8)) * 1000) : (story.created_at | fromNow).split(' ')[1] === 'minutes' ? Math.round(((story.points - 1) / Math.pow((9 + 2), 1.8)) * 1000) : Math.round(((story.points - 1) / Math.pow(((story.created_at | fromNow).split(' ')[0] * 19 + 2), 1.8)) * 1000)}}
12 |
13 | 14 |
by {{story.author}}
15 | 16 |
Subscribe to {{story.author}}
17 | 18 | 19 | 20 | {{story.num_comments ? story.num_comments + ' comments' : 'discuss'}} 21 | -------------------------------------------------------------------------------- /public/app/auth/auth.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

HN Feed

4 |

Sign up to save your followers

5 |
6 |
7 |
8 | 9 |
10 | 18 | 20 | 21 |
{{ badLoginMessage.data }}
22 |
23 |
24 | 25 |
26 |
27 |
28 | 36 | 37 | 38 | 39 |
{{ badSignupMessage.data }}
40 |
41 |
42 |
43 |
44 |

Greetings Hacker

45 |

{{username}}

46 |
47 |
48 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Goose-Windmill", 3 | "version": "1.0.0", 4 | "description": "Hacker News follower functionality", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Goose-Windmill/Goose-Windmill" 12 | }, 13 | "keywords": [ 14 | "hacker", 15 | "news", 16 | "feed" 17 | ], 18 | "author": "Goose Windmil", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Goose-Windmill/Goose-Windmill" 22 | }, 23 | "homepage": "https://github.com/Goose-Windmill/Goose-Windmill", 24 | "dependencies": { 25 | "bcrypt": "^0.8.2", 26 | "bcrypt-nodejs": "0.0.3", 27 | "bluebird": "^2.9.25", 28 | "body-parser": "^1.12.3", 29 | "express": "^4.12.3", 30 | "grunt-contrib-concat": "^0.5.1", 31 | "grunt-contrib-cssmin": "^0.12.3", 32 | "grunt-contrib-jshint": "^0.11.2", 33 | "grunt-contrib-uglify": "^0.9.1", 34 | "grunt-contrib-watch": "^0.6.1", 35 | "grunt-ng-annotate": "^0.10.0", 36 | "grunt-nodemon": "^0.4.0", 37 | "grunt-shell": "^1.1.2", 38 | "jwt-simple": "^0.3.0", 39 | "mongodb": "^2.0.28", 40 | "mongoose": "^4.0.2", 41 | "q": "^1.3.0", 42 | "request": "^2.55.0", 43 | "util": "^0.10.3" 44 | }, 45 | "devDependencies": { 46 | "chai": "^2.3.0", 47 | "grunt-bowercopy": "^1.2.0", 48 | "grunt-concurrent": "^1.0.0", 49 | "grunt-express-server": "^0.5.1", 50 | "grunt-karma": "^0.10.1", 51 | "grunt-mocha": "^0.4.12", 52 | "grunt-open": "^0.2.3", 53 | "grunt-protractor-runner": "^2.0.0", 54 | "grunt-run": "^0.3.0", 55 | "grunt-services": "^0.1.0", 56 | "grunt-shell-spawn": "^0.3.8", 57 | "karma": "^0.12.31", 58 | "karma-chai": "^0.1.0", 59 | "karma-chrome-launcher": "^0.1.10", 60 | "karma-mocha": "^0.1.10", 61 | "karma-mocha-reporter": "^1.0.2", 62 | "karma-requirejs": "^0.2.2", 63 | "karma-sinon": "^1.0.4", 64 | "karma-spec-reporter": "0.0.19", 65 | "mocha": "^2.2.4", 66 | "protractor": "^2.0.0", 67 | "requirejs": "^2.1.17", 68 | "sinon": "^1.14.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /HNMobile/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hacker News Mobile 4 | 5 | An Ionic Framework and Cordova project. 6 | 7 | 8 | Ionic Framework Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/app/services/links.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.linkService', []) 2 | 3 | .factory('Links', function($http, $interval) { 4 | var personalStories = []; 5 | var topStories = []; 6 | 7 | var getTopStories = function() { 8 | var url = '/api/cache/topStories' 9 | 10 | return $http({ 11 | method: 'GET', 12 | url: url 13 | }) 14 | .then(function(resp) { 15 | 16 | // Very important to not point topStories to a new array. 17 | // Instead, clear out the array, then push all the new 18 | // datum in place. There are pointers pointing to this array. 19 | topStories.splice(0, topStories.length); 20 | topStories.push.apply(topStories, resp.data); 21 | }); 22 | }; 23 | 24 | var getPersonalStories = function(usernames){ 25 | var query = 'http://hn.algolia.com/api/v1/search_by_date?hitsPerPage=500&tagFilters=(story,comment),('; 26 | var csv = arrToCSV(usernames); 27 | 28 | query += csv + ')'; 29 | 30 | return $http({ 31 | method: 'GET', 32 | url: query 33 | }) 34 | .then(function(resp) { 35 | angular.forEach(resp.data.hits, function(item){ 36 | // HN Comments don't have a title. So flag them as a comment. 37 | // This will come in handy when we decide how to render each item. 38 | if(item.title === null){ 39 | item.isComment = true; 40 | } 41 | }); 42 | 43 | // Very important to not point personalStories to a new array. 44 | // Instead, clear out the array, then push all the new 45 | // datum in place. There are pointers pointing to this array. 46 | personalStories.splice(0, personalStories.length); 47 | personalStories.push.apply(personalStories, resp.data.hits); 48 | }); 49 | }; 50 | 51 | var arrToCSV = function(arr){ 52 | var holder = []; 53 | 54 | for(var i = 0; i < arr.length; i++){ 55 | holder.push('author_' + arr[i]); 56 | } 57 | 58 | return holder.join(','); 59 | }; 60 | 61 | var init = function(){ 62 | 63 | $interval(function(){ 64 | getTopStories(); 65 | }, 300000); 66 | }; 67 | 68 | init(); 69 | 70 | return { 71 | getTopStories: getTopStories, 72 | getPersonalStories: getPersonalStories, 73 | personalStories: personalStories, 74 | topStories: topStories 75 | }; 76 | }); 77 | 78 | 79 | -------------------------------------------------------------------------------- /public/app/auth/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.auth', []) 2 | 3 | .controller('AuthController',function ($scope, $window, $location, Auth, Dashboard) { 4 | $scope.username = null; 5 | $scope.user = {}; 6 | $scope.newUser = {}; 7 | $scope.loggedIn = Auth.isAuth(); 8 | if($scope.loggedIn){ 9 | $scope.username = $window.localStorage.getItem('hfUser'); 10 | } 11 | $scope.badLogin = false; 12 | $scope.badLoginMessage = ''; 13 | $scope.badSignup = false; 14 | $scope.badSignupMessage = ''; 15 | 16 | $scope.signin = function () { 17 | Auth.signin($scope.user) 18 | .then(function (data) { 19 | $scope.badLogin = false; 20 | $scope.badLoginMessage = ''; 21 | $scope.badSignup = false; 22 | $scope.badSignupMessage = ''; 23 | $window.localStorage.setItem('com.hack', data.token); 24 | $window.localStorage.setItem('hfUsers', data.followers) 25 | $window.localStorage.setItem('hfUser', Auth.getUser()) 26 | 27 | Dashboard.init(data.followers.split(",")); 28 | 29 | $scope.loggedIn = true; 30 | $scope.user = {}; 31 | $scope.username = Auth.getUser() 32 | }) 33 | .catch(function (error) { 34 | $scope.badSignup = false; 35 | $scope.badSignupMessage = ''; 36 | $scope.badLogin = true; 37 | $scope.badLoginMessage = error; 38 | console.error(error); 39 | }); 40 | }; 41 | 42 | $scope.signup = function () { 43 | $scope.newUser.following = Dashboard.following.join(','); 44 | 45 | Auth.signup($scope.newUser) 46 | .then(function (data) { 47 | $scope.badLogin = false; 48 | $scope.badLoginMessage = ''; 49 | $scope.badSignup = false; 50 | $scope.badSignupMessage = ''; 51 | $window.localStorage.setItem('com.hack', data.token); 52 | $window.localStorage.setItem('hfUser', Auth.getUser()); 53 | 54 | $scope.loggedIn = true; 55 | $scope.newUser = {}; 56 | $scope.username = Auth.getUser() 57 | }) 58 | .catch(function (error) { 59 | $scope.badLogin = false; 60 | $scope.badLoginMessage = ''; 61 | $scope.badSignup = true; 62 | $scope.badSignupMessage = error; 63 | console.error(error); 64 | }); 65 | }; 66 | 67 | $scope.logout = function () { 68 | Auth.signout(); 69 | Dashboard.init(); 70 | $scope.loggedIn = false; 71 | $scope.username = null; 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_spinner.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Spinners 3 | * -------------------------------------------------- 4 | */ 5 | 6 | .spinner { 7 | svg { 8 | width: $spinner-width; 9 | height: $spinner-height; 10 | } 11 | 12 | stroke: $spinner-default-stroke; 13 | fill: $spinner-default-fill; 14 | 15 | &.spinner-light { 16 | stroke: $spinner-light-stroke; 17 | fill: $spinner-light-fill; 18 | } 19 | &.spinner-stable { 20 | stroke: $spinner-stable-stroke; 21 | fill: $spinner-stable-fill; 22 | } 23 | &.spinner-positive { 24 | stroke: $spinner-positive-stroke; 25 | fill: $spinner-positive-fill; 26 | } 27 | &.spinner-calm { 28 | stroke: $spinner-calm-stroke; 29 | fill: $spinner-calm-fill; 30 | } 31 | &.spinner-balanced { 32 | stroke: $spinner-balanced-stroke; 33 | fill: $spinner-balanced-fill; 34 | } 35 | &.spinner-assertive { 36 | stroke: $spinner-assertive-stroke; 37 | fill: $spinner-assertive-fill; 38 | } 39 | &.spinner-energized { 40 | stroke: $spinner-energized-stroke; 41 | fill: $spinner-energized-fill; 42 | } 43 | &.spinner-royal { 44 | stroke: $spinner-royal-stroke; 45 | fill: $spinner-royal-fill; 46 | } 47 | &.spinner-dark { 48 | stroke: $spinner-dark-stroke; 49 | fill: $spinner-dark-fill; 50 | } 51 | } 52 | 53 | .spinner-android { 54 | stroke: #4b8bf4; 55 | } 56 | 57 | .spinner-ios, 58 | .spinner-ios-small { 59 | stroke: #69717d; 60 | } 61 | 62 | .spinner-spiral { 63 | .stop1 { 64 | stop-color: $spinner-light-fill; 65 | stop-opacity: 0; 66 | } 67 | 68 | &.spinner-light { 69 | .stop1 { 70 | stop-color: $spinner-default-fill; 71 | } 72 | .stop2 { 73 | stop-color: $spinner-light-fill; 74 | } 75 | } 76 | &.spinner-stable .stop2 { 77 | stop-color: $spinner-stable-fill; 78 | } 79 | &.spinner-positive .stop2 { 80 | stop-color: $spinner-positive-fill; 81 | } 82 | &.spinner-calm .stop2 { 83 | stop-color: $spinner-calm-fill; 84 | } 85 | &.spinner-balanced .stop2 { 86 | stop-color: $spinner-balanced-fill; 87 | } 88 | &.spinner-assertive .stop2 { 89 | stop-color: $spinner-assertive-fill; 90 | } 91 | &.spinner-energized .stop2 { 92 | stop-color: $spinner-energized-fill; 93 | } 94 | &.spinner-royal .stop2 { 95 | stop-color: $spinner-royal-fill; 96 | } 97 | &.spinner-dark .stop2 { 98 | stop-color: $spinner-dark-fill; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_popup.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Popups 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .popup-container { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | bottom: 0; 12 | right: 0; 13 | background: rgba(0,0,0,0); 14 | 15 | @include display-flex(); 16 | @include justify-content(center); 17 | @include align-items(center); 18 | 19 | z-index: $z-index-popup; 20 | 21 | // Start hidden 22 | visibility: hidden; 23 | &.popup-showing { 24 | visibility: visible; 25 | } 26 | 27 | &.popup-hidden .popup { 28 | @include animation-name(scaleOut); 29 | @include animation-duration($popup-leave-animation-duration); 30 | @include animation-timing-function(ease-in-out); 31 | @include animation-fill-mode(both); 32 | } 33 | 34 | &.active .popup { 35 | @include animation-name(superScaleIn); 36 | @include animation-duration($popup-enter-animation-duration); 37 | @include animation-timing-function(ease-in-out); 38 | @include animation-fill-mode(both); 39 | } 40 | 41 | .popup { 42 | width: $popup-width; 43 | max-width: 100%; 44 | max-height: 90%; 45 | 46 | border-radius: $popup-border-radius; 47 | background-color: $popup-background-color; 48 | 49 | @include display-flex(); 50 | @include flex-direction(column); 51 | } 52 | 53 | input, 54 | textarea { 55 | width: 100%; 56 | } 57 | } 58 | 59 | .popup-head { 60 | padding: 15px 10px; 61 | border-bottom: 1px solid #eee; 62 | text-align: center; 63 | } 64 | .popup-title { 65 | margin: 0; 66 | padding: 0; 67 | font-size: 15px; 68 | } 69 | .popup-sub-title { 70 | margin: 5px 0 0 0; 71 | padding: 0; 72 | font-weight: normal; 73 | font-size: 11px; 74 | } 75 | .popup-body { 76 | padding: 10px; 77 | overflow: auto; 78 | } 79 | 80 | .popup-buttons { 81 | @include display-flex(); 82 | @include flex-direction(row); 83 | padding: 10px; 84 | min-height: $popup-button-min-height + 20; 85 | 86 | .button { 87 | @include flex(1); 88 | display: block; 89 | min-height: $popup-button-min-height; 90 | border-radius: $popup-button-border-radius; 91 | line-height: $popup-button-line-height; 92 | 93 | margin-right: 5px; 94 | &:last-child { 95 | margin-right: 0px; 96 | } 97 | } 98 | } 99 | 100 | .popup-open { 101 | pointer-events: none; 102 | 103 | &.modal-open .modal { 104 | pointer-events: none; 105 | } 106 | 107 | .popup-backdrop, .popup { 108 | pointer-events: auto; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue May 12 2015 12:19:59 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'requirejs', 'chai', 'sinon'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'test-main.js', 19 | 'public/lib/angular.js', 20 | 'public/lib/angular-mocks.js', 21 | 'public/lib/angular-route.min.js', 22 | 'public/lib/angular-ui-router.js', 23 | 'public/lib/angular-ui-router.min.js', 24 | 'public/lib/angular-jwt.js', 25 | 'public/lib/underscore-min.js', 26 | 'http://pc035860.github.io/angular-easyfb/angular-easyfb.min.js', 27 | {pattern: 'public/app/*.js', included: true}, 28 | {pattern: 'public/app/**/*.js', included: true}, 29 | {pattern: 'test/**/*.js', included: false} 30 | ], 31 | 32 | 33 | // list of files to exclude 34 | exclude: [ 35 | ], 36 | 37 | 38 | // preprocess matching files before serving them to the browser 39 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 40 | preprocessors: { 41 | }, 42 | 43 | 44 | // test results reporter to use 45 | // possible values: 'dots', 'progress' 46 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 47 | reporters: ['mocha'], 48 | 49 | 50 | // web server port 51 | port: 9876, 52 | 53 | 54 | // enable / disable colors in the output (reporters and logs) 55 | colors: true, 56 | 57 | 58 | // level of logging 59 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 60 | logLevel: config.LOG_INFO, 61 | 62 | 63 | // enable / disable watching file and executing tests whenever any file changes 64 | autoWatch: true, 65 | 66 | 67 | // start these browsers 68 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 69 | browsers: ['Chrome'], 70 | 71 | 72 | // Continuous Integration mode 73 | // if true, Karma captures browsers, runs the tests and exits 74 | singleRun: true, 75 | 76 | // any additional plugins needed for testing 77 | plugins: [ 78 | 'karma-mocha', 79 | 'karma-chai', 80 | 'karma-requirejs', 81 | 'karma-sinon', 82 | 'karma-chrome-launcher', 83 | 'karma-mocha-reporter' 84 | ] 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_modal.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Modals 4 | * -------------------------------------------------- 5 | * Modals are independent windows that slide in from off-screen. 6 | */ 7 | 8 | .modal-backdrop, 9 | .modal-backdrop-bg { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | z-index: $z-index-modal; 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | .modal-backdrop-bg { 19 | pointer-events: none; 20 | } 21 | 22 | .modal { 23 | display: block; 24 | position: absolute; 25 | top: 0; 26 | z-index: $z-index-modal; 27 | overflow: hidden; 28 | min-height: 100%; 29 | width: 100%; 30 | background-color: $modal-bg-color; 31 | } 32 | 33 | @media (min-width: $modal-inset-mode-break-point) { 34 | // inset mode is when the modal doesn't fill the entire 35 | // display but instead is centered within a large display 36 | .modal { 37 | top: $modal-inset-mode-top; 38 | right: $modal-inset-mode-right; 39 | bottom: $modal-inset-mode-bottom; 40 | left: $modal-inset-mode-left; 41 | min-height: $modal-inset-mode-min-height; 42 | width: (100% - $modal-inset-mode-left - $modal-inset-mode-right); 43 | } 44 | 45 | .modal.ng-leave-active { 46 | bottom: 0; 47 | } 48 | 49 | // remove ios header padding from inset header 50 | .platform-ios.platform-cordova .modal-wrapper .modal { 51 | .bar-header:not(.bar-subheader) { 52 | height: $bar-height; 53 | > * { 54 | margin-top: 0; 55 | } 56 | } 57 | .tabs-top > .tabs, 58 | .tabs.tabs-top { 59 | top: $bar-height; 60 | } 61 | .has-header, 62 | .bar-subheader { 63 | top: $bar-height; 64 | } 65 | .has-subheader { 66 | top: $bar-height + $bar-subheader-height; 67 | } 68 | .has-header.has-tabs-top { 69 | top: $bar-height + $tabs-height; 70 | } 71 | .has-header.has-subheader.has-tabs-top { 72 | top: $bar-height + $bar-subheader-height + $tabs-height; 73 | } 74 | } 75 | 76 | .modal-backdrop-bg { 77 | @include transition(opacity 300ms ease-in-out); 78 | background-color: $modal-backdrop-bg-active; 79 | opacity: 0; 80 | } 81 | 82 | .active .modal-backdrop-bg { 83 | opacity: 0.5; 84 | } 85 | } 86 | 87 | // disable clicks on all but the modal 88 | .modal-open { 89 | pointer-events: none; 90 | 91 | .modal, 92 | .modal-backdrop { 93 | pointer-events: auto; 94 | } 95 | // prevent clicks on modal when loading overlay is active though 96 | &.loading-active { 97 | .modal, 98 | .modal-backdrop { 99 | pointer-events: none; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /public/app/app.js: -------------------------------------------------------------------------------- 1 | angular.module('hack', [ 2 | 'ui.router', 3 | 'hack.topStories', 4 | 'hack.personal', 5 | 'hack.userDashboard', 6 | 'hack.linkService', 7 | 'hack.authService', 8 | 'hack.dashboardService', 9 | 'hack.graphService', 10 | 'hack.tabs', 11 | 'hack.auth', 12 | 'ezfb' 13 | ]) 14 | .config(function(ezfbProvider, $stateProvider, $urlRouterProvider,$httpProvider) { 15 | ezfbProvider.setInitParams({ 16 | appId: '836420059740734' 17 | }); 18 | $urlRouterProvider.otherwise('/'); 19 | 20 | $stateProvider 21 | 22 | .state('/personal', { 23 | url: "/personal", 24 | templateUrl: "app/personal/personal.html", 25 | controller: 'PersonalController' 26 | }) 27 | .state('/topStories', { 28 | url: "/topStories", 29 | templateUrl: "app/topStories/topStories.html", 30 | controller: 'TopStoriesController' 31 | }) 32 | .state('/', { 33 | url: "/", 34 | templateUrl: "app/topStories/topStories.html", 35 | controller: 'TopStoriesController' 36 | }); 37 | 38 | 39 | $httpProvider.interceptors.push(['$q', '$location', '$window', function($q, $location, $window) { 40 | return { 41 | 'request': function (config) { 42 | if (config.url.indexOf('algolia') === -1) { 43 | config.headers = config.headers || {}; 44 | if ($window.localStorage.getItem('com.hack')) { 45 | config.headers.Authorization = 'Bearer ' + $window.localStorage.getItem('com.hack'); 46 | config.headers['x-access-token'] = $window.localStorage.getItem('com.hack'); 47 | } 48 | } 49 | return config; 50 | }, 51 | 'responseError': function(response) { 52 | if(response.status === 401 || response.status === 403) { 53 | $location.path('/signin'); 54 | } 55 | return $q.reject(response); 56 | } 57 | }; 58 | }]); 59 | }) 60 | 61 | 62 | .filter('fromNow', function(){ 63 | return function(date){ 64 | return humanized_time_span(new Date(date)); 65 | } 66 | }) 67 | 68 | .filter('htmlsafe', ['$sce', function ($sce) { 69 | return function (text) { 70 | return $sce.trustAsHtml(text); 71 | }; 72 | }]) 73 | 74 | .directive('rotate', function () { 75 | return { 76 | restrict: 'A', 77 | link: function (scope, element, attrs) { 78 | scope.$watch(attrs.degrees, function (rotateDegrees) { 79 | var r = 'rotate(' + rotateDegrees + 'deg)'; 80 | console.log(r); 81 | element.css({ 82 | '-moz-transform': r, 83 | '-webkit-transform': r, 84 | '-o-transform': r, 85 | '-ms-transform': r 86 | }); 87 | }); 88 | } 89 | } 90 | }); -------------------------------------------------------------------------------- /server/cache/cacheModel.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | //In server memory of Hacker News current top stories 4 | var topStories = []; 5 | 6 | //Set headers 7 | var headers = { 8 | 'User-Agent': 'Hacker Feed', 9 | 'Content-Type': 'application/json' 10 | }; 11 | 12 | module.exports = { 13 | //Access function for model data 14 | getTopStories: function(callback) { 15 | if (topStories.length) { 16 | callback(null,topStories); 17 | } else { 18 | callback(new Error('Top Stories not cached!')); 19 | } 20 | }, 21 | 22 | // The top news stories data is retrieved from the Algolia API, however it does not include 23 | // Hacker News' ranking algorithm. The data retrieved from Algolia is sorted according to the 24 | // ranking on the firebase API 25 | 26 | updateTopStories: function() { 27 | var storyOrderUrl = 'https://hacker-news.firebaseio.com/v0/topstories.json'; 28 | 29 | // Configure API request 30 | var options = { 31 | url: storyOrderUrl, 32 | method: 'GET', 33 | headers: headers 34 | }; 35 | 36 | // Perform the firebase API request 37 | request(options, function(error, response, html){ 38 | if (error) { 39 | throw (error); 40 | } 41 | var data = JSON.parse(response.body); 42 | var storyOrder = data; 43 | 44 | // Generate algolia search API URL 45 | var storyUrl = 'http://hn.algolia.com/api/v1/search?hitsPerPage=500&tagFilters=story,('; 46 | var storyUrlIds = []; 47 | for(var i = 0; i < storyOrder.length; i++) { 48 | storyUrlIds.push('story_' + storyOrder[i]); 49 | } 50 | storyUrl += storyUrlIds.join(',') + ')'; 51 | options.url = storyUrl; 52 | 53 | request(options, function(error, response, html){ 54 | if (error) { 55 | throw (error); 56 | } 57 | // Reorder the retrieved stories to match the hacker news front page 58 | 59 | var data = JSON.parse(response.body); 60 | var index; 61 | var indexMap = data.hits.map(function(obj) { 62 | return obj.objectID; 63 | }); 64 | 65 | // Clear out previous top stories 66 | topStories.length = 0; 67 | 68 | //storyOrder matches hacker news front page. Find data related to the story ID 69 | //in the incoming response data 70 | for(var i = 0; i < storyOrder.length; i++) { 71 | index = indexMap.indexOf(String(storyOrder[i])); 72 | var item = data.hits[index]; 73 | if(item){ 74 | topStories.push(data.hits[index]); 75 | } 76 | } 77 | console.log("Top Stories Updated"); 78 | }); 79 | }).on('error', function() { console.log ('err')}); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /server/users/userController.js: -------------------------------------------------------------------------------- 1 | var User = require('./userModel.js'); 2 | var jwt = require('jwt-simple'); 3 | 4 | module.exports = { 5 | app: {}, 6 | //Handles new user account generation 7 | signup: function(request, response, next) { 8 | 9 | var username = request.body.username; 10 | var password = request.body.password; 11 | var following = request.body.following; 12 | 13 | var params = { 14 | username: username, 15 | password: password, 16 | following: following 17 | }; 18 | 19 | //First, determine if the username is available 20 | User.findOne({username: username}, function(err, user) { 21 | //User already exists, try again! 22 | if(user) { 23 | //Figure out a way for the client to redirect to the signup page 24 | //and inform the user that this username is already in use. 25 | response.status(400).send('Username already exists'); 26 | } else { 27 | //If it is not in use, create the user in the database 28 | User.prototype.createUser(params, function(err){ 29 | if(!err){ 30 | var token = jwt.encode({user: username}, module.exports.app.get('jwtTokenSecret')); 31 | console.log('token: ' + token); 32 | response.status(200).send({token: token}); 33 | } else { 34 | response.status(400).send(err); 35 | } 36 | }); 37 | } 38 | }); 39 | }, 40 | 41 | //Interact with the database to validate username/password combination 42 | signin: function(request, response, next) { 43 | var username = request.body.username; 44 | var password = request.body.password; 45 | 46 | User.prototype.signin(username, password, function(err, results){ 47 | if(!err){ 48 | console.log('Signed in'); 49 | console.log(results); 50 | var token = jwt.encode({user: username}, module.exports.app.get('jwtTokenSecret')); 51 | console.log('token: ' + token); 52 | response.status(200).send({followers: results, token: token}); 53 | } else { 54 | console.log('Sign In error'); 55 | response.status(400).send(err); 56 | } 57 | }) 58 | }, 59 | 60 | //Controller tells the model to update the database when the user adds or 61 | //removes users from their following list 62 | updateFollowing: function(request, response, next) { 63 | if (!request.user) { 64 | console.log('Not authenticated'); 65 | response.status(400).send('Not authenticated'); 66 | } 67 | var username = request.user.username; 68 | var following = request.body.following; 69 | 70 | User.prototype.updateFollowing(username, following, function(err, results){ 71 | if(!err){ 72 | console.log('User following data updated'); 73 | response.status(200).end(); 74 | } else { 75 | console.log('User following data update ERROR'); 76 | response.status(400).send(err); 77 | } 78 | }); 79 | } 80 | }; -------------------------------------------------------------------------------- /HNMobile/www/css/style.css: -------------------------------------------------------------------------------- 1 | /* Empty. Add your own CSS if you like */ 2 | 3 | body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .item .story-title { 8 | white-space: normal; 9 | font-size: 17px; 10 | font-weight: bold; 11 | } 12 | 13 | .item .story-title-link { 14 | color: #0079B2; 15 | } 16 | 17 | .item .story-url { 18 | float: left; 19 | font-size: 13px; 20 | color: #908D91; 21 | margin-top: -5px; 22 | } 23 | 24 | .item .score-container { 25 | clear: both; 26 | float: right; 27 | color: #908D91; 28 | font-size: 10px; 29 | margin-top: -6px; 30 | } 31 | 32 | .item .score { 33 | color: #404040; 34 | font-size: 13px; 35 | font-weight: bold; 36 | } 37 | 38 | .item .story-author { 39 | clear: both; 40 | float: left; 41 | font-size: 13px; 42 | margin-top: -20px; 43 | } 44 | 45 | .item .follow-author { 46 | float: left; 47 | background: #ECECEC; 48 | font-size: 10px; 49 | padding: 0 3px; 50 | border: 1px solid #908D91; 51 | border-radius: 4px; 52 | cursor: pointer; 53 | } 54 | 55 | .item .story-comments { 56 | float: right; 57 | text-align: center; 58 | font-size: 14px; 59 | color: #0079B2; 60 | } 61 | 62 | .points-bar { 63 | float: right; 64 | background: #FF8C00; 65 | height: 8px; 66 | max-width: 170px; 67 | } 68 | 69 | /* PERSONAL SECTION */ 70 | 71 | .following-list { 72 | text-align: center; 73 | background: #404040; 74 | color: #FFF; 75 | padding-top: 7px; 76 | padding-bottom: 10px; 77 | } 78 | 79 | .following-list h2 { 80 | font-size: 15px; 81 | color: #FFF; 82 | } 83 | 84 | .following-list-users { 85 | line-height: 1.6em; 86 | width: 280px; 87 | margin: 0 auto; 88 | } 89 | 90 | .following-list-user { 91 | background: #FFF; 92 | color: #000; 93 | margin: 0 5px; 94 | padding: 0 4px; 95 | border-radius: 3px; 96 | } 97 | 98 | .user-x { 99 | color: #F43B44; 100 | font-size: 15px; 101 | font-weight: bold; 102 | } 103 | 104 | .comment-container { 105 | background: #A2A2A2; 106 | padding: 0; 107 | } 108 | 109 | .comment { 110 | white-space: normal; 111 | text-align: center; 112 | background: #FFF; 113 | font-size: 14px; 114 | margin: 0 auto; 115 | padding: 4px 10px 4px 10px; 116 | } 117 | 118 | .comment-meta { 119 | color: #A9A9A9; 120 | font-size: 12px; 121 | } 122 | 123 | .comment-text { 124 | text-align: left; 125 | } 126 | 127 | .add-a-comment { 128 | color: #0079B2; 129 | } 130 | 131 | /* COMMENTS PAGE */ 132 | 133 | .comment-story-title-container { 134 | background: #FF8C00; 135 | color: #FFF; 136 | font-size: 15px; 137 | padding: 8px 20px 11px 20px; 138 | text-align: center; 139 | } 140 | 141 | .comment-story-title { 142 | color: #000; 143 | font-size: 19px; 144 | } 145 | 146 | .comments-link { 147 | color: #0079B2; 148 | font-size: 13px; 149 | } 150 | 151 | /* ABOUT */ 152 | 153 | .about-info { 154 | white-space: normal; 155 | } -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_list.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Lists 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .list { 8 | position: relative; 9 | padding-top: $item-border-width; 10 | padding-bottom: $item-border-width; 11 | padding-left: 0; // reset padding because ul and ol 12 | margin-bottom: 20px; 13 | } 14 | .list:last-child { 15 | margin-bottom: 0px; 16 | &.card{ 17 | margin-bottom:40px; 18 | } 19 | } 20 | 21 | 22 | /** 23 | * List Header 24 | * -------------------------------------------------- 25 | */ 26 | 27 | .list-header { 28 | margin-top: $list-header-margin-top; 29 | padding: $list-header-padding; 30 | background-color: $list-header-bg; 31 | color: $list-header-color; 32 | font-weight: bold; 33 | } 34 | 35 | // when its a card make sure it doesn't duplicate top and bottom borders 36 | .card.list .list-item { 37 | padding-right: 1px; 38 | padding-left: 1px; 39 | } 40 | 41 | 42 | /** 43 | * Cards and Inset Lists 44 | * -------------------------------------------------- 45 | * A card and list-inset are close to the same thing, except a card as a box shadow. 46 | */ 47 | 48 | .card, 49 | .list-inset { 50 | overflow: hidden; 51 | margin: ($content-padding * 2) $content-padding; 52 | border-radius: $card-border-radius; 53 | background-color: $card-body-bg; 54 | } 55 | 56 | .card { 57 | padding-top: $item-border-width; 58 | padding-bottom: $item-border-width; 59 | box-shadow: $card-box-shadow; 60 | 61 | .item { 62 | border-left: 0; 63 | border-right: 0; 64 | } 65 | .item:first-child { 66 | border-top: 0; 67 | } 68 | .item:last-child { 69 | border-bottom: 0; 70 | } 71 | } 72 | 73 | .padding { 74 | .card, .list-inset { 75 | margin-left: 0; 76 | margin-right: 0; 77 | } 78 | } 79 | 80 | .card .item, 81 | .list-inset .item, 82 | .padding > .list .item 83 | { 84 | &:first-child { 85 | border-top-left-radius: $card-border-radius; 86 | border-top-right-radius: $card-border-radius; 87 | 88 | .item-content { 89 | border-top-left-radius: $card-border-radius; 90 | border-top-right-radius: $card-border-radius; 91 | } 92 | } 93 | &:last-child { 94 | border-bottom-right-radius: $card-border-radius; 95 | border-bottom-left-radius: $card-border-radius; 96 | 97 | .item-content { 98 | border-bottom-right-radius: $card-border-radius; 99 | border-bottom-left-radius: $card-border-radius; 100 | } 101 | } 102 | } 103 | 104 | .card .item:last-child, 105 | .list-inset .item:last-child { 106 | margin-bottom: $item-border-width * -1; 107 | } 108 | 109 | .card .item, 110 | .list-inset .item, 111 | .padding > .list .item, 112 | .padding-horizontal > .list .item { 113 | margin-right: 0; 114 | margin-left: 0; 115 | 116 | &.item-input input { 117 | padding-right: 44px; 118 | } 119 | } 120 | .padding-left > .list .item { 121 | margin-left: 0; 122 | } 123 | .padding-right > .list .item { 124 | margin-right: 0; 125 | } 126 | -------------------------------------------------------------------------------- /HNMobile/www/js/services/followers.js: -------------------------------------------------------------------------------- 1 | // HOW OUR FOLLOWING SYSTEM WORKS: 2 | // We want users to be able to follow people before they even 3 | // log in, because who actually has time to decide on a username/password? 4 | 5 | // So, we do this by saving the users that they follow into localStorage. 6 | // On signup, we'll send the users string in localStorage to our server 7 | // which wil save them to a database. 8 | 9 | angular.module('hack.followService', []) 10 | 11 | .factory('Followers', function($http, $window) { 12 | var following = []; 13 | 14 | var updateFollowing = function(){ 15 | var user = $window.localStorage.getItem('com.hack'); 16 | 17 | if(!!user){ 18 | var data = { 19 | username: user, 20 | following: localStorageUsers() 21 | }; 22 | 23 | $http({ 24 | method: 'POST', 25 | url: '/api/users/updateFollowing', 26 | data: data 27 | }); 28 | } 29 | }; 30 | 31 | var addFollower = function(username){ 32 | var localFollowing = localStorageUsers(); 33 | 34 | if (!localFollowing.includes(username) && following.indexOf(username) === -1) { 35 | localFollowing += ',' + username 36 | $window.localStorage.setItem('hfUsers', localFollowing); 37 | following.push(username); 38 | } 39 | 40 | // makes call to database to mirror our changes 41 | updateFollowing(); 42 | }; 43 | 44 | var removeFollower = function(username){ 45 | var localFollowing = localStorageUsers(); 46 | 47 | if (localFollowing.includes(username) && following.indexOf(username) > -1) { 48 | following.splice(following.indexOf(username), 1); 49 | 50 | localFollowing = localFollowing.split(','); 51 | localFollowing.splice(localFollowing.indexOf(username), 1).join(','); 52 | $window.localStorage.setItem('hfUsers', localFollowing); 53 | } 54 | 55 | // makes call to database to mirror our changes 56 | updateFollowing(); 57 | }; 58 | 59 | var localStorageUsers = function(){ 60 | return $window.localStorage.getItem('hfUsers'); 61 | } 62 | 63 | 64 | // this function takes the csv in localStorage and turns it into an array. 65 | // There are pointers pointing to the 'following' array. The 'following' array 66 | // is how our controllers listen for changes and dynamically update the DOM. 67 | // (because you can't listen to localStorage changes) 68 | var localToArr = function(){ 69 | if(!localStorageUsers()){ 70 | // If the person is a new visitor, set pg and sama as the default 71 | // people to follow. Kinda like Tom on MySpace. Except less creepy. 72 | $window.localStorage.setItem('hfUsers', 'pg,sama'); 73 | } 74 | 75 | var users = localStorageUsers().split(','); 76 | 77 | following.splice(0, following.length); 78 | following.push.apply(following, users); 79 | } 80 | 81 | var init = function(){ 82 | localToArr(); 83 | }; 84 | 85 | init(); 86 | 87 | return { 88 | following: following, 89 | addFollower: addFollower, 90 | removeFollower: removeFollower, 91 | localToArr: localToArr 92 | } 93 | }) 94 | -------------------------------------------------------------------------------- /HNMobile/hooks/after_prepare/010_add_platform_class.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Add Platform Class 4 | // v1.0 5 | // Automatically adds the platform class to the body tag 6 | // after the `prepare` command. By placing the platform CSS classes 7 | // directly in the HTML built for the platform, it speeds up 8 | // rendering the correct layout/style for the specific platform 9 | // instead of waiting for the JS to figure out the correct classes. 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | 14 | var rootdir = process.argv[2]; 15 | 16 | function addPlatformBodyTag(indexPath, platform) { 17 | // add the platform class to the body tag 18 | try { 19 | var platformClass = 'platform-' + platform; 20 | var cordovaClass = 'platform-cordova platform-webview'; 21 | 22 | var html = fs.readFileSync(indexPath, 'utf8'); 23 | 24 | var bodyTag = findBodyTag(html); 25 | if(!bodyTag) return; // no opening body tag, something's wrong 26 | 27 | if(bodyTag.indexOf(platformClass) > -1) return; // already added 28 | 29 | var newBodyTag = bodyTag; 30 | 31 | var classAttr = findClassAttr(bodyTag); 32 | if(classAttr) { 33 | // body tag has existing class attribute, add the classname 34 | var endingQuote = classAttr.substring(classAttr.length-1); 35 | var newClassAttr = classAttr.substring(0, classAttr.length-1); 36 | newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; 37 | newBodyTag = bodyTag.replace(classAttr, newClassAttr); 38 | 39 | } else { 40 | // add class attribute to the body tag 41 | newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); 42 | } 43 | 44 | html = html.replace(bodyTag, newBodyTag); 45 | 46 | fs.writeFileSync(indexPath, html, 'utf8'); 47 | 48 | process.stdout.write('add to body class: ' + platformClass + '\n'); 49 | } catch(e) { 50 | process.stdout.write(e); 51 | } 52 | } 53 | 54 | function findBodyTag(html) { 55 | // get the body tag 56 | try{ 57 | return html.match(/])(.*?)>/gi)[0]; 58 | }catch(e){} 59 | } 60 | 61 | function findClassAttr(bodyTag) { 62 | // get the body tag's class attribute 63 | try{ 64 | return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; 65 | }catch(e){} 66 | } 67 | 68 | if (rootdir) { 69 | 70 | // go through each of the platform directories that have been prepared 71 | var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); 72 | 73 | for(var x=0; x .scroll{ 83 | &.overscroll{ 84 | position:fixed; 85 | } 86 | -webkit-overflow-scrolling:touch; 87 | width:100%; 88 | } 89 | 90 | @-webkit-keyframes refresh-spin { 91 | 0% { -webkit-transform: translate3d(0,0,0) rotate(0); } 92 | 100% { -webkit-transform: translate3d(0,0,0) rotate(180deg); } 93 | } 94 | 95 | @keyframes refresh-spin { 96 | 0% { transform: translate3d(0,0,0) rotate(0); } 97 | 100% { transform: translate3d(0,0,0) rotate(180deg); } 98 | } 99 | 100 | @-webkit-keyframes refresh-spin-back { 101 | 0% { -webkit-transform: translate3d(0,0,0) rotate(180deg); } 102 | 100% { -webkit-transform: translate3d(0,0,0) rotate(0); } 103 | } 104 | 105 | @keyframes refresh-spin-back { 106 | 0% { transform: translate3d(0,0,0) rotate(180deg); } 107 | 100% { transform: translate3d(0,0,0) rotate(0); } 108 | } 109 | -------------------------------------------------------------------------------- /server/users/userModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Promise = require('bluebird'); 3 | var bcrypt = require('bcrypt-nodejs'); 4 | 5 | //Simple database of user data. Password is stored as a hash/salt combination. 6 | //The following value is a comma separated value of posters that the user is following. 7 | var UserSchema = mongoose.Schema({ 8 | username: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | hashword: { 14 | type: String, 15 | required: true 16 | }, 17 | following: { 18 | type: String, 19 | required: true 20 | } 21 | }); 22 | 23 | var User = mongoose.model('users', UserSchema); 24 | 25 | //Model generates new users on signup. This function is only called after the controller 26 | //determines that the username is not taken. 27 | //params: an object that contains a string of the username, plain text string of the password, 28 | // and a comma separated string of users that the user is following 29 | User.prototype.createUser = function (params, callback){ 30 | //User password is stored as a hash/salt combination 31 | bcrypt.genSalt(10, function (err, salt) { 32 | bcrypt.hash(params.password, salt, null, function(err, hash){ 33 | if(hash) { 34 | var newUser = new User({ 35 | username: params.username, 36 | hashword: hash, 37 | following: params.following 38 | }); 39 | if (!newUser.following) { 40 | newUser.following = ['pg', 'sama']; 41 | } 42 | newUser.save(function(err,results){ 43 | //Relay user creation success/failure back to the controller 44 | callback(err, results); 45 | }); 46 | } else { 47 | //bcrypt.hash error reporting 48 | callback(err); 49 | } 50 | }); 51 | }); 52 | }; 53 | 54 | //Model handles finding the user and password comparison 55 | User.prototype.signin = function (username, password, callback){ 56 | //Find if the user exists 57 | User.findOne({'username':username},function(err,user){ 58 | //If the user exists, compare hashed passwords 59 | if(user){ 60 | bcrypt.compare(password, user.hashword, function(err, res) { 61 | if (res) { 62 | //If correct, return the list of hacker news posters that the user is following 63 | callback(null, user.following); 64 | } else { 65 | //If not correct, handle error 66 | callback('Incorrect password', null); 67 | } 68 | }); 69 | } else { 70 | //If the user doesn't exist, handle error 71 | callback('Username not found', null); 72 | } 73 | }); 74 | }; 75 | 76 | //Update database when the user adds or removes users from their following list 77 | //Method operates on the assumption that the .signin has validated the username. 78 | User.prototype.updateFollowing = function (username, following, callback){ 79 | //Find the user in the database 80 | //Could try to refactor to .findOneAndUpdate or .findOneAndModify 81 | User.findOne({username: username}, function(err, user) { 82 | if (err) { 83 | callback(err); 84 | } else if (user) { 85 | //Modify and save the database 86 | user.following = following; 87 | user.save(); 88 | callback(err,user); 89 | } 90 | }); 91 | }; 92 | 93 | module.exports = User; -------------------------------------------------------------------------------- /HNMobile/hooks/README.md: -------------------------------------------------------------------------------- 1 | 21 | # Cordova Hooks 22 | 23 | This directory may contain scripts used to customize cordova commands. This 24 | directory used to exist at `.cordova/hooks`, but has now been moved to the 25 | project root. Any scripts you add to these directories will be executed before 26 | and after the commands corresponding to the directory name. Useful for 27 | integrating your own build systems or integrating with version control systems. 28 | 29 | __Remember__: Make your scripts executable. 30 | 31 | ## Hook Directories 32 | The following subdirectories will be used for hooks: 33 | 34 | after_build/ 35 | after_compile/ 36 | after_docs/ 37 | after_emulate/ 38 | after_platform_add/ 39 | after_platform_rm/ 40 | after_platform_ls/ 41 | after_plugin_add/ 42 | after_plugin_ls/ 43 | after_plugin_rm/ 44 | after_plugin_search/ 45 | after_prepare/ 46 | after_run/ 47 | after_serve/ 48 | before_build/ 49 | before_compile/ 50 | before_docs/ 51 | before_emulate/ 52 | before_platform_add/ 53 | before_platform_rm/ 54 | before_platform_ls/ 55 | before_plugin_add/ 56 | before_plugin_ls/ 57 | before_plugin_rm/ 58 | before_plugin_search/ 59 | before_prepare/ 60 | before_run/ 61 | before_serve/ 62 | pre_package/ <-- Windows 8 and Windows Phone only. 63 | 64 | ## Script Interface 65 | 66 | All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: 67 | 68 | * CORDOVA_VERSION - The version of the Cordova-CLI. 69 | * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). 70 | * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) 71 | * CORDOVA_HOOK - Path to the hook that is being executed. 72 | * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) 73 | 74 | If a script returns a non-zero exit code, then the parent cordova command will be aborted. 75 | 76 | 77 | ## Writing hooks 78 | 79 | We highly recommend writting your hooks using Node.js so that they are 80 | cross-platform. Some good examples are shown here: 81 | 82 | [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) 83 | 84 | -------------------------------------------------------------------------------- /HNMobile/www/js/app.js: -------------------------------------------------------------------------------- 1 | // Ionic Starter App 2 | 3 | // angular.module is a global place for creating, registering and retrieving Angular modules 4 | // 'starter' is the name of this angular module example (also set in a attribute in index.html) 5 | // the 2nd parameter is an array of 'requires' 6 | // 'starter.controllers' is found in controllers.js 7 | angular.module('hack', [ 8 | 'ionic', 9 | 'hack.linkService', 10 | 'hack.followService', 11 | 'hack.controllers']) 12 | 13 | .run(function($ionicPlatform) { 14 | $ionicPlatform.ready(function() { 15 | // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard 16 | // for form inputs) 17 | if (window.cordova && window.cordova.plugins.Keyboard) { 18 | cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); 19 | } 20 | if (window.StatusBar) { 21 | // org.apache.cordova.statusbar required 22 | StatusBar.styleDefault(); 23 | } 24 | }); 25 | }) 26 | 27 | .config(function($stateProvider, $urlRouterProvider) { 28 | $stateProvider 29 | 30 | .state('app', { 31 | url: "/app", 32 | abstract: true, 33 | templateUrl: "templates/menu.html", 34 | controller: 'AppCtrl' 35 | }) 36 | 37 | .state('app.topStories', { 38 | url: "/top-stories", 39 | views: { 40 | 'menuContent': { 41 | templateUrl: "templates/top-stories.html", 42 | controller: 'TopStoriesCtrl' 43 | } 44 | } 45 | }) 46 | 47 | .state('app.comments', { 48 | url: "/comments/:storyId", 49 | views: { 50 | 'menuContent': { 51 | templateUrl: "templates/comments.html", 52 | controller: 'CommentsCtrl' 53 | } 54 | } 55 | }) 56 | 57 | .state('app.mostPoints', { 58 | url: "/most-points", 59 | views: { 60 | 'menuContent': { 61 | templateUrl: "templates/most-points.html", 62 | controller: 'MostPointsCtrl' 63 | } 64 | } 65 | }) 66 | 67 | .state('app.mostComments', { 68 | url: "/most-comments", 69 | views: { 70 | 'menuContent': { 71 | templateUrl: "templates/most-comments.html", 72 | controller: 'MostCommentsCtrl' 73 | } 74 | } 75 | }) 76 | 77 | .state('app.mostRecent', { 78 | url: "/most-recent", 79 | views: { 80 | 'menuContent': { 81 | templateUrl: "templates/most-recent.html", 82 | controller: 'MostRecentCtrl' 83 | } 84 | } 85 | }) 86 | 87 | .state('app.personal', { 88 | url: "/personal", 89 | views: { 90 | 'menuContent': { 91 | templateUrl: "templates/personal.html", 92 | controller: 'PersonalCtrl' 93 | } 94 | } 95 | }) 96 | 97 | .state('app.about', { 98 | url: "/about", 99 | views: { 100 | 'menuContent': { 101 | templateUrl: "templates/about.html", 102 | } 103 | } 104 | }) 105 | 106 | // if none of the above states are matched, use this as the fallback 107 | $urlRouterProvider.otherwise('/app/top-stories'); 108 | }) 109 | 110 | .filter('fromNow', function(){ 111 | return function(date){ 112 | return humanized_time_span(new Date(date)); 113 | } 114 | }) 115 | 116 | .filter('htmlsafe', ['$sce', function ($sce) { 117 | return function (text) { 118 | return $sce.trustAsHtml(text); 119 | }; 120 | }]); -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_select.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Select 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .item-select { 8 | position: relative; 9 | 10 | select { 11 | @include appearance(none); 12 | position: absolute; 13 | top: 0; 14 | bottom: 0; 15 | right: 0; 16 | padding: ($item-padding - 2) ($item-padding * 3) ($item-padding) $item-padding; 17 | max-width: 65%; 18 | 19 | border: none; 20 | background: $item-default-bg; 21 | color: #333; 22 | 23 | // hack to hide default dropdown arrow in FF 24 | text-indent: .01px; 25 | text-overflow: ''; 26 | 27 | white-space: nowrap; 28 | font-size: $font-size-base; 29 | 30 | cursor: pointer; 31 | direction: rtl; // right align the select text 32 | } 33 | 34 | select::-ms-expand { 35 | // hide default dropdown arrow in IE 36 | display: none; 37 | } 38 | 39 | option { 40 | direction: ltr; 41 | } 42 | 43 | &:after { 44 | position: absolute; 45 | top: 50%; 46 | right: $item-padding; 47 | margin-top: -3px; 48 | width: 0; 49 | height: 0; 50 | border-top: 5px solid; 51 | border-right: 5px solid rgba(0, 0, 0, 0); 52 | border-left: 5px solid rgba(0, 0, 0, 0); 53 | color: #999; 54 | content: ""; 55 | pointer-events: none; 56 | } 57 | &.item-light { 58 | select{ 59 | background:$item-light-bg; 60 | color:$item-light-text; 61 | } 62 | } 63 | &.item-stable { 64 | select{ 65 | background:$item-stable-bg; 66 | color:$item-stable-text; 67 | } 68 | &:after, .input-label{ 69 | color:darken($item-stable-border,30%); 70 | } 71 | } 72 | &.item-positive { 73 | select{ 74 | background:$item-positive-bg; 75 | color:$item-positive-text; 76 | } 77 | &:after, .input-label{ 78 | color:$item-positive-text; 79 | } 80 | } 81 | &.item-calm { 82 | select{ 83 | background:$item-calm-bg; 84 | color:$item-calm-text; 85 | } 86 | &:after, .input-label{ 87 | color:$item-calm-text; 88 | } 89 | } 90 | &.item-assertive { 91 | select{ 92 | background:$item-assertive-bg; 93 | color:$item-assertive-text; 94 | } 95 | &:after, .input-label{ 96 | color:$item-assertive-text; 97 | } 98 | } 99 | &.item-balanced { 100 | select{ 101 | background:$item-balanced-bg; 102 | color:$item-balanced-text; 103 | } 104 | &:after, .input-label{ 105 | color:$item-balanced-text; 106 | } 107 | } 108 | &.item-energized { 109 | select{ 110 | background:$item-energized-bg; 111 | color:$item-energized-text; 112 | } 113 | &:after, .input-label{ 114 | color:$item-energized-text; 115 | } 116 | } 117 | &.item-royal { 118 | select{ 119 | background:$item-royal-bg; 120 | color:$item-royal-text; 121 | } 122 | &:after, .input-label{ 123 | color:$item-royal-text; 124 | } 125 | } 126 | &.item-dark { 127 | select{ 128 | background:$item-dark-bg; 129 | color:$item-dark-text; 130 | } 131 | &:after, .input-label{ 132 | color:$item-dark-text; 133 | } 134 | } 135 | } 136 | 137 | select { 138 | &[multiple], 139 | &[size] { 140 | height: auto; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/js/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.3.13 3 | (c) 2010-2014 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,d,B){'use strict';function D(f,q){q=q||{};d.forEach(q,function(d,h){delete q[h]});for(var h in f)!f.hasOwnProperty(h)||"$"===h.charAt(0)&&"$"===h.charAt(1)||(q[h]=f[h]);return q}var w=d.$$minErr("$resource"),C=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;d.module("ngResource",["ng"]).provider("$resource",function(){var f=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}}; 7 | this.$get=["$http","$q",function(q,h){function t(d,g){this.template=d;this.defaults=s({},f.defaults,g);this.urlParams={}}function v(x,g,l,m){function c(b,k){var c={};k=s({},g,k);r(k,function(a,k){u(a)&&(a=a());var d;if(a&&a.charAt&&"@"==a.charAt(0)){d=b;var e=a.substr(1);if(null==e||""===e||"hasOwnProperty"===e||!C.test("."+e))throw w("badmember",e);for(var e=e.split("."),n=0,g=e.length;n -1) { 54 | following.splice(following.indexOf(username), 1); 55 | // makes call to database to mirror our changes 56 | updateFollowing(); 57 | } 58 | 59 | }; 60 | 61 | 62 | var selected = []; 63 | 64 | var addSelected = function(username){ 65 | selected.push(username); 66 | } 67 | var removeSelected = function(username){ 68 | var userIndex = selected.indexOf(username) 69 | selected.splice(userIndex, 1); 70 | } 71 | 72 | var localStorageUsers = function(){ 73 | return $window.localStorage.getItem('hfUsers'); 74 | } 75 | 76 | 77 | // this function takes the csv in localStorage and turns it into an array. 78 | // There are pointers pointing to the 'following' array. The 'following' array 79 | // is how our controllers listen for changes and dynamically update the DOM. 80 | // (because you can't listen to localStorage changes) 81 | var localToArr = function(){ 82 | // if(!localStorageUsers()){ 83 | // // If the person is a new visitor, set pg and sama as the default 84 | // // people to follow. Kinda like Tom on MySpace. Except less creepy. 85 | // $window.localStorage.setItem('hfUsers', 'pg,sama'); 86 | // } 87 | if (localStorageUsers()) { 88 | var users = localStorageUsers().split(","); 89 | return users; 90 | } 91 | } 92 | 93 | var init = function(saved_followers){ 94 | var users = saved_followers || localToArr(); 95 | following.splice(0, following.length); 96 | following.push.apply(following, users); 97 | // refresh personal stories 98 | Links.getPersonalStories(following); 99 | $window.localStorage.setItem('hfUsers', following); 100 | }; 101 | 102 | init(); 103 | 104 | return { 105 | following: following, 106 | addFollower: addFollower, 107 | removeFollower: removeFollower, 108 | localToArr: localToArr, 109 | addSelected: addSelected, 110 | removeSelected: removeSelected, 111 | init: init 112 | } 113 | }) 114 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_grid.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid 3 | * -------------------------------------------------- 4 | * Using flexbox for the grid, inspired by Philip Walton: 5 | * http://philipwalton.github.io/solved-by-flexbox/demos/grids/ 6 | * By default each .col within a .row will evenly take up 7 | * available width, and the height of each .col with take 8 | * up the height of the tallest .col in the same .row. 9 | */ 10 | 11 | .row { 12 | @include display-flex(); 13 | padding: ($grid-padding-width / 2); 14 | width: 100%; 15 | } 16 | 17 | .row-wrap { 18 | @include flex-wrap(wrap); 19 | } 20 | 21 | .row-no-padding { 22 | padding: 0; 23 | 24 | > .col { 25 | padding: 0; 26 | } 27 | } 28 | 29 | .row + .row { 30 | margin-top: ($grid-padding-width / 2) * -1; 31 | padding-top: 0; 32 | } 33 | 34 | .col { 35 | @include flex(1); 36 | display: block; 37 | padding: ($grid-padding-width / 2); 38 | width: 100%; 39 | } 40 | 41 | 42 | /* Vertically Align Columns */ 43 | /* .row-* vertically aligns every .col in the .row */ 44 | .row-top { 45 | @include align-items(flex-start); 46 | } 47 | .row-bottom { 48 | @include align-items(flex-end); 49 | } 50 | .row-center { 51 | @include align-items(center); 52 | } 53 | .row-stretch { 54 | @include align-items(stretch); 55 | } 56 | .row-baseline { 57 | @include align-items(baseline); 58 | } 59 | 60 | /* .col-* vertically aligns an individual .col */ 61 | .col-top { 62 | @include align-self(flex-start); 63 | } 64 | .col-bottom { 65 | @include align-self(flex-end); 66 | } 67 | .col-center { 68 | @include align-self(center); 69 | } 70 | 71 | /* Column Offsets */ 72 | .col-offset-10 { 73 | margin-left: 10%; 74 | } 75 | .col-offset-20 { 76 | margin-left: 20%; 77 | } 78 | .col-offset-25 { 79 | margin-left: 25%; 80 | } 81 | .col-offset-33, .col-offset-34 { 82 | margin-left: 33.3333%; 83 | } 84 | .col-offset-50 { 85 | margin-left: 50%; 86 | } 87 | .col-offset-66, .col-offset-67 { 88 | margin-left: 66.6666%; 89 | } 90 | .col-offset-75 { 91 | margin-left: 75%; 92 | } 93 | .col-offset-80 { 94 | margin-left: 80%; 95 | } 96 | .col-offset-90 { 97 | margin-left: 90%; 98 | } 99 | 100 | 101 | /* Explicit Column Percent Sizes */ 102 | /* By default each grid column will evenly distribute */ 103 | /* across the grid. However, you can specify individual */ 104 | /* columns to take up a certain size of the available area */ 105 | .col-10 { 106 | @include flex(0, 0, 10%); 107 | max-width: 10%; 108 | } 109 | .col-20 { 110 | @include flex(0, 0, 20%); 111 | max-width: 20%; 112 | } 113 | .col-25 { 114 | @include flex(0, 0, 25%); 115 | max-width: 25%; 116 | } 117 | .col-33, .col-34 { 118 | @include flex(0, 0, 33.3333%); 119 | max-width: 33.3333%; 120 | } 121 | .col-50 { 122 | @include flex(0, 0, 50%); 123 | max-width: 50%; 124 | } 125 | .col-66, .col-67 { 126 | @include flex(0, 0, 66.6666%); 127 | max-width: 66.6666%; 128 | } 129 | .col-75 { 130 | @include flex(0, 0, 75%); 131 | max-width: 75%; 132 | } 133 | .col-80 { 134 | @include flex(0, 0, 80%); 135 | max-width: 80%; 136 | } 137 | .col-90 { 138 | @include flex(0, 0, 90%); 139 | max-width: 90%; 140 | } 141 | 142 | 143 | /* Responsive Grid Classes */ 144 | /* Adding a class of responsive-X to a row */ 145 | /* will trigger the flex-direction to */ 146 | /* change to column and add some margin */ 147 | /* to any columns in the row for clearity */ 148 | 149 | @include responsive-grid-break('.responsive-sm', $grid-responsive-sm-break); 150 | @include responsive-grid-break('.responsive-md', $grid-responsive-md-break); 151 | @include responsive-grid-break('.responsive-lg', $grid-responsive-lg-break); 152 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_type.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Typography 4 | * -------------------------------------------------- 5 | */ 6 | 7 | 8 | // Body text 9 | // ------------------------- 10 | 11 | p { 12 | margin: 0 0 ($line-height-computed / 2); 13 | } 14 | 15 | 16 | // Emphasis & misc 17 | // ------------------------- 18 | 19 | small { font-size: 85%; } 20 | cite { font-style: normal; } 21 | 22 | 23 | // Alignment 24 | // ------------------------- 25 | 26 | .text-left { text-align: left; } 27 | .text-right { text-align: right; } 28 | .text-center { text-align: center; } 29 | 30 | 31 | // Headings 32 | // ------------------------- 33 | 34 | h1, h2, h3, h4, h5, h6, 35 | .h1, .h2, .h3, .h4, .h5, .h6 { 36 | color: $base-color; 37 | font-weight: $headings-font-weight; 38 | font-family: $headings-font-family; 39 | line-height: $headings-line-height; 40 | 41 | small { 42 | font-weight: normal; 43 | line-height: 1; 44 | } 45 | } 46 | 47 | h1, .h1, 48 | h2, .h2, 49 | h3, .h3 { 50 | margin-top: $line-height-computed; 51 | margin-bottom: ($line-height-computed / 2); 52 | 53 | &:first-child { 54 | margin-top: 0; 55 | } 56 | 57 | + h1, + .h1, 58 | + h2, + .h2, 59 | + h3, + .h3 { 60 | margin-top: ($line-height-computed / 2); 61 | } 62 | } 63 | 64 | h4, .h4, 65 | h5, .h5, 66 | h6, .h6 { 67 | margin-top: ($line-height-computed / 2); 68 | margin-bottom: ($line-height-computed / 2); 69 | } 70 | 71 | h1, .h1 { font-size: floor($font-size-base * 2.60); } // ~36px 72 | h2, .h2 { font-size: floor($font-size-base * 2.15); } // ~30px 73 | h3, .h3 { font-size: ceil($font-size-base * 1.70); } // ~24px 74 | h4, .h4 { font-size: ceil($font-size-base * 1.25); } // ~18px 75 | h5, .h5 { font-size: $font-size-base; } 76 | h6, .h6 { font-size: ceil($font-size-base * 0.85); } // ~12px 77 | 78 | h1 small, .h1 small { font-size: ceil($font-size-base * 1.70); } // ~24px 79 | h2 small, .h2 small { font-size: ceil($font-size-base * 1.25); } // ~18px 80 | h3 small, .h3 small, 81 | h4 small, .h4 small { font-size: $font-size-base; } 82 | 83 | 84 | // Description Lists 85 | // ------------------------- 86 | 87 | dl { 88 | margin-bottom: $line-height-computed; 89 | } 90 | dt, 91 | dd { 92 | line-height: $line-height-base; 93 | } 94 | dt { 95 | font-weight: bold; 96 | } 97 | 98 | 99 | // Blockquotes 100 | // ------------------------- 101 | 102 | blockquote { 103 | margin: 0 0 $line-height-computed; 104 | padding: ($line-height-computed / 2) $line-height-computed; 105 | border-left: 5px solid gray; 106 | 107 | p { 108 | font-weight: 300; 109 | font-size: ($font-size-base * 1.25); 110 | line-height: 1.25; 111 | } 112 | 113 | p:last-child { 114 | margin-bottom: 0; 115 | } 116 | 117 | small { 118 | display: block; 119 | line-height: $line-height-base; 120 | &:before { 121 | content: '\2014 \00A0';// EM DASH, NBSP; 122 | } 123 | } 124 | } 125 | 126 | 127 | // Quotes 128 | // ------------------------- 129 | 130 | q:before, 131 | q:after, 132 | blockquote:before, 133 | blockquote:after { 134 | content: ""; 135 | } 136 | 137 | 138 | // Addresses 139 | // ------------------------- 140 | 141 | address { 142 | display: block; 143 | margin-bottom: $line-height-computed; 144 | font-style: normal; 145 | line-height: $line-height-base; 146 | } 147 | 148 | 149 | // Links 150 | // ------------------------- 151 | 152 | a.subdued { 153 | padding-right: 10px; 154 | color: #888; 155 | text-decoration: none; 156 | 157 | &:hover { 158 | text-decoration: none; 159 | } 160 | &:last-child { 161 | padding-right: 0; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_action-sheet.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Action Sheets 3 | * -------------------------------------------------- 4 | */ 5 | 6 | .action-sheet-backdrop { 7 | @include transition(background-color 150ms ease-in-out); 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | z-index: $z-index-action-sheet; 12 | width: 100%; 13 | height: 100%; 14 | background-color: rgba(0,0,0,0); 15 | 16 | &.active { 17 | background-color: rgba(0,0,0,0.4); 18 | } 19 | } 20 | 21 | .action-sheet-wrapper { 22 | @include translate3d(0, 100%, 0); 23 | @include transition(all cubic-bezier(.36, .66, .04, 1) 500ms); 24 | position: absolute; 25 | bottom: 0; 26 | left: 0; 27 | right: 0; 28 | width: 100%; 29 | max-width: 500px; 30 | margin: auto; 31 | } 32 | 33 | .action-sheet-up { 34 | @include translate3d(0, 0, 0); 35 | } 36 | 37 | .action-sheet { 38 | margin-left: $sheet-margin; 39 | margin-right: $sheet-margin; 40 | width: auto; 41 | z-index: $z-index-action-sheet; 42 | overflow: hidden; 43 | 44 | .button { 45 | display: block; 46 | padding: 1px; 47 | width: 100%; 48 | border-radius: 0; 49 | border-color: $sheet-options-border-color; 50 | background-color: transparent; 51 | 52 | color: $sheet-options-text-color; 53 | font-size: 21px; 54 | 55 | &:hover { 56 | color: $sheet-options-text-color; 57 | } 58 | &.destructive { 59 | color: #ff3b30; 60 | &:hover { 61 | color: #ff3b30; 62 | } 63 | } 64 | } 65 | 66 | .button.active, .button.activated { 67 | box-shadow: none; 68 | border-color: $sheet-options-border-color; 69 | color: $sheet-options-text-color; 70 | background: $sheet-options-bg-active-color; 71 | } 72 | } 73 | 74 | .action-sheet-has-icons .icon { 75 | position: absolute; 76 | left: 16px; 77 | } 78 | 79 | .action-sheet-title { 80 | padding: $sheet-margin * 2; 81 | color: #8f8f8f; 82 | text-align: center; 83 | font-size: 13px; 84 | } 85 | 86 | .action-sheet-group { 87 | margin-bottom: $sheet-margin; 88 | border-radius: $sheet-border-radius; 89 | background-color: #fff; 90 | overflow: hidden; 91 | 92 | .button { 93 | border-width: 1px 0px 0px 0px; 94 | } 95 | .button:first-child:last-child { 96 | border-width: 0; 97 | } 98 | } 99 | 100 | .action-sheet-options { 101 | background: $sheet-options-bg-color; 102 | } 103 | 104 | .action-sheet-cancel { 105 | .button { 106 | font-weight: 500; 107 | } 108 | } 109 | 110 | .action-sheet-open { 111 | pointer-events: none; 112 | 113 | &.modal-open .modal { 114 | pointer-events: none; 115 | } 116 | 117 | .action-sheet-backdrop { 118 | pointer-events: auto; 119 | } 120 | } 121 | 122 | 123 | .platform-android { 124 | 125 | .action-sheet-backdrop.active { 126 | background-color: rgba(0,0,0,0.2); 127 | } 128 | 129 | .action-sheet { 130 | margin: 0; 131 | 132 | .action-sheet-title, 133 | .button { 134 | text-align: left; 135 | border-color: transparent; 136 | font-size: 16px; 137 | color: inherit; 138 | } 139 | 140 | .action-sheet-title { 141 | font-size: 14px; 142 | padding: 16px; 143 | color: #666; 144 | } 145 | 146 | .button.active, 147 | .button.activated { 148 | background: #e8e8e8; 149 | } 150 | } 151 | 152 | .action-sheet-group { 153 | margin: 0; 154 | border-radius: 0; 155 | background-color: #fafafa; 156 | } 157 | 158 | .action-sheet-cancel { 159 | display: none; 160 | } 161 | 162 | .action-sheet-has-icons { 163 | 164 | .button { 165 | padding-left: 56px; 166 | } 167 | 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /public/lib/angular-jwt.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 4 | // Create all modules and define dependencies to make sure they exist 5 | // and are loaded in the correct order to satisfy dependency injection 6 | // before all nested files are concatenated by Grunt 7 | 8 | // Modules 9 | angular.module('angular-jwt', 10 | [ 11 | 'angular-jwt.interceptor', 12 | 'angular-jwt.jwt' 13 | ]); 14 | 15 | angular.module('angular-jwt.interceptor', []) 16 | .provider('jwtInterceptor', function() { 17 | 18 | this.authHeader = 'Authorization'; 19 | this.authPrefix = 'Bearer '; 20 | this.tokenGetter = function() { 21 | return null; 22 | } 23 | 24 | var config = this; 25 | 26 | this.$get = ["$q", "$injector", "$rootScope", function ($q, $injector, $rootScope) { 27 | return { 28 | request: function (request) { 29 | if (request.skipAuthorization) { 30 | return request; 31 | } 32 | 33 | request.headers = request.headers || {}; 34 | // Already has an Authorization header 35 | if (request.headers[config.authHeader]) { 36 | return request; 37 | } 38 | 39 | var tokenPromise = $q.when($injector.invoke(config.tokenGetter, this, { 40 | config: request 41 | })); 42 | 43 | return tokenPromise.then(function(token) { 44 | if (token) { 45 | request.headers[config.authHeader] = config.authPrefix + token; 46 | } 47 | return request; 48 | }); 49 | }, 50 | responseError: function (response) { 51 | // handle the case where the user is not authenticated 52 | if (response.status === 401) { 53 | $rootScope.$broadcast('unauthenticated', response); 54 | } 55 | return $q.reject(response); 56 | } 57 | }; 58 | }]; 59 | }); 60 | 61 | angular.module('angular-jwt.jwt', []) 62 | .service('jwtHelper', function() { 63 | 64 | this.urlBase64Decode = function(str) { 65 | var output = str.replace('-', '+').replace('_', '/'); 66 | switch (output.length % 4) { 67 | case 0: { break; } 68 | case 2: { output += '=='; break; } 69 | case 3: { output += '='; break; } 70 | default: { 71 | throw 'Illegal base64url string!'; 72 | } 73 | } 74 | // return window.atob(output); //polifyll https://github.com/davidchambers/Base64.js 75 | return decodeURIComponent(escape(window.atob(output))); //polifyll https://github.com/davidchambers/Base64.js 76 | } 77 | 78 | 79 | this.decodeToken = function(token) { 80 | var parts = token.split('.'); 81 | 82 | if (parts.length !== 3) { 83 | throw new Error('JWT must have 3 parts'); 84 | } 85 | 86 | var decoded = this.urlBase64Decode(parts[1]); 87 | if (!decoded) { 88 | throw new Error('Cannot decode the token'); 89 | } 90 | 91 | return JSON.parse(decoded); 92 | } 93 | 94 | this.getTokenExpirationDate = function(token) { 95 | var decoded; 96 | decoded = this.decodeToken(token); 97 | 98 | if(!decoded.exp) { 99 | return null; 100 | } 101 | 102 | var d = new Date(0); // The 0 here is the key, which sets the date to the epoch 103 | d.setUTCSeconds(decoded.exp); 104 | 105 | return d; 106 | }; 107 | 108 | this.isTokenExpired = function(token) { 109 | var d = this.getTokenExpirationDate(token); 110 | 111 | if (!d) { 112 | return false; 113 | } 114 | 115 | // Token expired? 116 | return !(d.valueOf() > new Date().valueOf()); 117 | }; 118 | }); 119 | 120 | }()); -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_popover.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Popovers 4 | * -------------------------------------------------- 5 | * Popovers are independent views which float over content 6 | */ 7 | 8 | .popover-backdrop { 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | z-index: $z-index-popover; 13 | width: 100%; 14 | height: 100%; 15 | background-color: $popover-backdrop-bg-inactive; 16 | 17 | &.active { 18 | background-color: $popover-backdrop-bg-active; 19 | } 20 | } 21 | 22 | .popover { 23 | position: absolute; 24 | top: 25%; 25 | left: 50%; 26 | z-index: $z-index-popover; 27 | display: block; 28 | margin-top: 12px; 29 | margin-left: -$popover-width / 2; 30 | height: $popover-height; 31 | width: $popover-width; 32 | background-color: $popover-bg-color; 33 | box-shadow: $popover-box-shadow; 34 | opacity: 0; 35 | 36 | .item:first-child { 37 | border-top: 0; 38 | } 39 | 40 | .item:last-child { 41 | border-bottom: 0; 42 | } 43 | 44 | &.popover-bottom { 45 | margin-top: -12px; 46 | } 47 | } 48 | 49 | 50 | // Set popover border-radius 51 | .popover, 52 | .popover .bar-header { 53 | border-radius: $popover-border-radius; 54 | } 55 | .popover .scroll-content { 56 | z-index: 1; 57 | margin: 2px 0; 58 | } 59 | .popover .bar-header { 60 | border-bottom-right-radius: 0; 61 | border-bottom-left-radius: 0; 62 | } 63 | .popover .has-header { 64 | border-top-right-radius: 0; 65 | border-top-left-radius: 0; 66 | } 67 | .popover-arrow { 68 | display: none; 69 | } 70 | 71 | 72 | // iOS Popover 73 | .platform-ios { 74 | 75 | .popover { 76 | box-shadow: $popover-box-shadow-ios; 77 | border-radius: $popover-border-radius-ios; 78 | } 79 | .popover .bar-header { 80 | @include border-top-radius($popover-border-radius-ios); 81 | } 82 | .popover .scroll-content { 83 | margin: 8px 0; 84 | border-radius: $popover-border-radius-ios; 85 | } 86 | .popover .scroll-content.has-header { 87 | margin-top: 0; 88 | } 89 | .popover-arrow { 90 | position: absolute; 91 | display: block; 92 | top: -17px; 93 | width: 30px; 94 | height: 19px; 95 | overflow: hidden; 96 | 97 | &:after { 98 | position: absolute; 99 | top: 12px; 100 | left: 5px; 101 | width: 20px; 102 | height: 20px; 103 | background-color: $popover-bg-color; 104 | border-radius: 3px; 105 | content: ''; 106 | @include rotate(-45deg); 107 | } 108 | } 109 | .popover-bottom .popover-arrow { 110 | top: auto; 111 | bottom: -10px; 112 | &:after { 113 | top: -6px; 114 | } 115 | } 116 | } 117 | 118 | 119 | // Android Popover 120 | .platform-android { 121 | 122 | .popover { 123 | margin-top: -32px; 124 | background-color: $popover-bg-color-android; 125 | box-shadow: $popover-box-shadow-android; 126 | 127 | .item { 128 | border-color: $popover-bg-color-android; 129 | background-color: $popover-bg-color-android; 130 | color: #4d4d4d; 131 | } 132 | &.popover-bottom { 133 | margin-top: 32px; 134 | } 135 | } 136 | 137 | .popover-backdrop, 138 | .popover-backdrop.active { 139 | background-color: transparent; 140 | } 141 | } 142 | 143 | 144 | // disable clicks on all but the popover 145 | .popover-open { 146 | pointer-events: none; 147 | 148 | .popover, 149 | .popover-backdrop { 150 | pointer-events: auto; 151 | } 152 | // prevent clicks on popover when loading overlay is active though 153 | &.loading-active { 154 | .popover, 155 | .popover-backdrop { 156 | pointer-events: none; 157 | } 158 | } 159 | } 160 | 161 | 162 | // wider popover on larger viewports 163 | @media (min-width: $popover-large-break-point) { 164 | .popover { 165 | width: $popover-large-width; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /_PRESS-RELEASE.md: -------------------------------------------------------------------------------- 1 | # Hacker Feed # 2 | 3 | 18 | 19 | Browse Hacker News more efficiently by telling Hacker Feed what you want to follow. 20 | 21 | ## Summary ## 22 | The Hacker News interface is outdated and does not have any options for personalize filtering. Hacker Feed exists to make Hacker News users time browsing more efficient by personalizing their feed. 23 | 24 | ## Problem ## 25 | Hacker News users do not have any options for filtering their content. 26 | 27 | ## Solution ## 28 | Hacker Feed allows its users to personalize their Hacker News reading experience by allowing them to follow users and topics that they find interesting. 29 | 30 | ## Quote from You ## 31 | "Hack your Hacker News experience." - Sid 32 | 33 | ## How to Get Started ## 34 | Go to the website, sign up for an account, and start following your favorite HN users and topics. 35 | 36 | ## Customer Quote ## 37 | "I cut my time browsing Hacker News by 50% while also reading only the interesting content." - Paul Graham 38 | 39 | ## Closing and Call to Action ## 40 | Visit LINK, sign up for an account, follow your favorite users and topics, and watch Hacker Feed do all your work for you! 41 | -------------------------------------------------------------------------------- /public/lib/human-time.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 by Will Tomlins 2 | // 3 | // Github profile: http://github.com/layam 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | 24 | function humanized_time_span(date, ref_date, date_formats, time_units) { 25 | //Date Formats must be be ordered smallest -> largest and must end in a format with ceiling of null 26 | date_formats = date_formats || { 27 | past: [ 28 | { ceiling: 60, text: "$seconds seconds ago" }, 29 | { ceiling: 3600, text: "$minutes minutes ago" }, 30 | { ceiling: 86400, text: "$hours hours ago" }, 31 | { ceiling: 2629744, text: "$days days ago" }, 32 | { ceiling: 31556926, text: "$months months ago" }, 33 | { ceiling: null, text: "$years years ago" } 34 | ], 35 | future: [ 36 | { ceiling: 60, text: "in $seconds seconds" }, 37 | { ceiling: 3600, text: "in $minutes minutes" }, 38 | { ceiling: 86400, text: "in $hours hours" }, 39 | { ceiling: 2629744, text: "in $days days" }, 40 | { ceiling: 31556926, text: "in $months months" }, 41 | { ceiling: null, text: "in $years years" } 42 | ] 43 | }; 44 | //Time units must be be ordered largest -> smallest 45 | time_units = time_units || [ 46 | [31556926, 'years'], 47 | [2629744, 'months'], 48 | [86400, 'days'], 49 | [3600, 'hours'], 50 | [60, 'minutes'], 51 | [1, 'seconds'] 52 | ]; 53 | 54 | date = new Date(date); 55 | ref_date = ref_date ? new Date(ref_date) : new Date(); 56 | var seconds_difference = (ref_date - date) / 1000; 57 | 58 | var tense = 'past'; 59 | if (seconds_difference < 0) { 60 | tense = 'future'; 61 | seconds_difference = 0-seconds_difference; 62 | } 63 | 64 | function get_format() { 65 | for (var i=0; i largest and must end in a format with ceiling of null 26 | date_formats = date_formats || { 27 | past: [ 28 | { ceiling: 60, text: "$seconds seconds ago" }, 29 | { ceiling: 3600, text: "$minutes minutes ago" }, 30 | { ceiling: 86400, text: "$hours hours ago" }, 31 | { ceiling: 2629744, text: "$days days ago" }, 32 | { ceiling: 31556926, text: "$months months ago" }, 33 | { ceiling: null, text: "$years years ago" } 34 | ], 35 | future: [ 36 | { ceiling: 60, text: "in $seconds seconds" }, 37 | { ceiling: 3600, text: "in $minutes minutes" }, 38 | { ceiling: 86400, text: "in $hours hours" }, 39 | { ceiling: 2629744, text: "in $days days" }, 40 | { ceiling: 31556926, text: "in $months months" }, 41 | { ceiling: null, text: "in $years years" } 42 | ] 43 | }; 44 | //Time units must be be ordered largest -> smallest 45 | time_units = time_units || [ 46 | [31556926, 'years'], 47 | [2629744, 'months'], 48 | [86400, 'days'], 49 | [3600, 'hours'], 50 | [60, 'minutes'], 51 | [1, 'seconds'] 52 | ]; 53 | 54 | date = new Date(date); 55 | ref_date = ref_date ? new Date(ref_date) : new Date(); 56 | var seconds_difference = (ref_date - date) / 1000; 57 | 58 | var tense = 'past'; 59 | if (seconds_difference < 0) { 60 | tense = 'future'; 61 | seconds_difference = 0-seconds_difference; 62 | } 63 | 64 | function get_format() { 65 | for (var i=0; i -1) { 53 | following.splice(following.indexOf(username), 1); 54 | // makes call to database to mirror our changes 55 | updateFollowing(); 56 | } 57 | 58 | }; 59 | 60 | var userFeeds = []; 61 | var saveFeed =function(){}; 62 | var deleteFeed = function(){}; 63 | 64 | 65 | var userHashes = []; 66 | var updateUserHashes = function(){}; 67 | var addHash = function(hash){}; 68 | var removeHash = function(hash){}; 69 | 70 | var selectedUsers = []; 71 | 72 | var addSelectedUser = function(username){ 73 | selectedUsers.push(username); 74 | } 75 | var removeSelectedUser = function(username){ 76 | var userIndex = selectedUsers.indexOf(username) 77 | selectedUsers.splice(userIndex, 1); 78 | } 79 | 80 | var selectedhashes = []; 81 | var addSelectedHash = function(){}; 82 | var removeSelectedHash = function(){}; 83 | 84 | var deleteSelected = function(){ 85 | for(var i = 0; i < selectedUsers.length;i++){ 86 | removeFollower(selectedUsers[i]); 87 | } 88 | }; 89 | 90 | 91 | 92 | var localStorageUsers = function(){ 93 | return $window.localStorage.getItem('hfUsers'); 94 | } 95 | 96 | 97 | // this function takes the csv in localStorage and turns it into an array. 98 | // There are pointers pointing to the 'following' array. The 'following' array 99 | // is how our controllers listen for changes and dynamically update the DOM. 100 | // (because you can't listen to localStorage changes) 101 | var localToArr = function(){ 102 | // if(!localStorageUsers()){ 103 | // // If the person is a new visitor, set pg and sama as the default 104 | // // people to follow. Kinda like Tom on MySpace. Except less creepy. 105 | // $window.localStorage.setItem('hfUsers', 'pg,sama'); 106 | // } 107 | if (localStorageUsers()) { 108 | var users = localStorageUsers().split(","); 109 | return users; 110 | } 111 | } 112 | 113 | var init = function(saved_followers){ 114 | var users = saved_followers || localToArr(); 115 | following.splice(0, following.length); 116 | following.push.apply(following, users); 117 | // refresh personal stories 118 | Links.getPersonalStories(following); 119 | $window.localStorage.setItem('hfUsers', following); 120 | }; 121 | 122 | init(); 123 | 124 | return { 125 | following: following, 126 | addFollower: addFollower, 127 | removeFollower: removeFollower, 128 | localToArr: localToArr, 129 | addSelectedUser: addSelectedUser, 130 | removeSelectedUser: removeSelectedUser, 131 | addSelectedHash: addSelectedHash, 132 | removeSelectedHash: removeSelectedHash, 133 | saveFeed: saveFeed, 134 | deleteFeed: deleteFeed, 135 | deleteSelected: deleteSelected, 136 | init: init 137 | } 138 | }) 139 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_transitions.scss: -------------------------------------------------------------------------------- 1 | 2 | // iOS View Transitions 3 | // ------------------------------- 4 | 5 | $ios-transition-duration: 500ms !default; 6 | $ios-transition-timing-function: cubic-bezier(.36, .66, .04, 1) !default; 7 | $ios-transition-container-bg-color: #000 !default; 8 | 9 | 10 | [nav-view-transition="ios"] { 11 | 12 | [nav-view="entering"], 13 | [nav-view="leaving"] { 14 | @include transition-duration( $ios-transition-duration ); 15 | @include transition-timing-function( $ios-transition-timing-function ); 16 | -webkit-transition-property: opacity, -webkit-transform, box-shadow; 17 | transition-property: opacity, transform, box-shadow; 18 | } 19 | 20 | &[nav-view-direction="forward"], 21 | &[nav-view-direction="back"] { 22 | background-color: $ios-transition-container-bg-color; 23 | } 24 | 25 | [nav-view="active"], 26 | &[nav-view-direction="forward"] [nav-view="entering"], 27 | &[nav-view-direction="back"] [nav-view="leaving"] { 28 | z-index: $z-index-view-above; 29 | } 30 | 31 | &[nav-view-direction="back"] [nav-view="entering"], 32 | &[nav-view-direction="forward"] [nav-view="leaving"] { 33 | z-index: $z-index-view-below; 34 | } 35 | 36 | } 37 | 38 | 39 | 40 | // iOS Nav Bar Transitions 41 | // ------------------------------- 42 | 43 | [nav-bar-transition="ios"] { 44 | 45 | .title, 46 | .buttons, 47 | .back-text { 48 | @include transition-duration( $ios-transition-duration ); 49 | @include transition-timing-function( $ios-transition-timing-function ); 50 | -webkit-transition-property: opacity, -webkit-transform; 51 | transition-property: opacity, transform; 52 | } 53 | 54 | [nav-bar="active"], 55 | [nav-bar="entering"] { 56 | z-index: $z-index-bar-above; 57 | 58 | .bar { 59 | background: transparent; 60 | } 61 | } 62 | 63 | [nav-bar="cached"] { 64 | display: block; 65 | 66 | .header-item { 67 | display: none; 68 | } 69 | } 70 | 71 | } 72 | 73 | 74 | 75 | // Android View Transitions 76 | // ------------------------------- 77 | 78 | $android-transition-duration: 200ms !default; 79 | $android-transition-timing-function: cubic-bezier(0.4, 0.6, 0.2, 1) !default; 80 | 81 | 82 | [nav-view-transition="android"] { 83 | 84 | [nav-view="entering"], 85 | [nav-view="leaving"] { 86 | @include transition-duration( $android-transition-duration ); 87 | @include transition-timing-function( $android-transition-timing-function ); 88 | -webkit-transition-property: -webkit-transform; 89 | transition-property: transform; 90 | } 91 | 92 | [nav-view="active"], 93 | &[nav-view-direction="forward"] [nav-view="entering"], 94 | &[nav-view-direction="back"] [nav-view="leaving"] { 95 | z-index: $z-index-view-above; 96 | } 97 | 98 | &[nav-view-direction="back"] [nav-view="entering"], 99 | &[nav-view-direction="forward"] [nav-view="leaving"] { 100 | z-index: $z-index-view-below; 101 | } 102 | 103 | } 104 | 105 | 106 | 107 | // Android Nav Bar Transitions 108 | // ------------------------------- 109 | 110 | [nav-bar-transition="android"] { 111 | 112 | .title, 113 | .buttons { 114 | @include transition-duration( $android-transition-duration ); 115 | @include transition-timing-function( $android-transition-timing-function ); 116 | -webkit-transition-property: opacity; 117 | transition-property: opacity; 118 | } 119 | 120 | [nav-bar="active"], 121 | [nav-bar="entering"] { 122 | z-index: $z-index-bar-above; 123 | 124 | .bar { 125 | background: transparent; 126 | } 127 | } 128 | 129 | [nav-bar="cached"] { 130 | display: block; 131 | 132 | .header-item { 133 | display: none; 134 | } 135 | } 136 | 137 | } 138 | 139 | 140 | 141 | // Nav Swipe 142 | // ------------------------------- 143 | 144 | [nav-swipe="fast"] { 145 | [nav-view], 146 | .title, 147 | .buttons, 148 | .back-text { 149 | @include transition-duration(50ms); 150 | @include transition-timing-function(linear); 151 | } 152 | } 153 | 154 | [nav-swipe="slow"] { 155 | [nav-view], 156 | .title, 157 | .buttons, 158 | .back-text { 159 | @include transition-duration(160ms); 160 | @include transition-timing-function(linear); 161 | } 162 | } 163 | 164 | 165 | 166 | // Transition Settings 167 | // ------------------------------- 168 | 169 | [nav-view="cached"], 170 | [nav-bar="cached"] { 171 | display: none; 172 | } 173 | 174 | [nav-view="stage"] { 175 | opacity: 0; 176 | @include transition-duration( 0 ); 177 | } 178 | 179 | [nav-bar="stage"] { 180 | .title, 181 | .buttons, 182 | .back-text { 183 | position: absolute; 184 | opacity: 0; 185 | @include transition-duration(0s); 186 | } 187 | } 188 | 189 | -------------------------------------------------------------------------------- /HNMobile/www/js/services/links.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.linkService', []) 2 | 3 | .factory('Links', function($http, $interval/*, Followers*/) { 4 | var personalStories = []; 5 | var topStories = []; 6 | var comments = []; 7 | var treeToArray = []; 8 | 9 | var getTopStories = function() { 10 | var url = 'http://hnmobileapp.herokuapp.com/api/cache/topStories'; 11 | 12 | return $http({ 13 | method: 'GET', 14 | url: url 15 | }) 16 | .then(function(resp) { 17 | 18 | // Very important to not point topStories to a new array. 19 | // Instead, clear out the array, then push all the new 20 | // datum in place. There are pointers pointing to this array. 21 | topStories.splice(0, topStories.length); 22 | topStories.push.apply(topStories, resp.data); 23 | }); 24 | }; 25 | 26 | var getPersonalStories = function(usernames){ 27 | var query = 'http://hn.algolia.com/api/v1/search_by_date?hitsPerPage=500&tagFilters=(story,comment),('; 28 | var csv = arrToCSV(usernames); 29 | 30 | query += csv + ')'; 31 | 32 | return $http({ 33 | method: 'GET', 34 | url: query 35 | }) 36 | .then(function(resp) { 37 | angular.forEach(resp.data.hits, function(item){ 38 | // HN Comments don't have a title. So flag them as a comment. 39 | // This will come in handy when we decide how to render each item. 40 | if(item.title === null){ 41 | item.isComment = true; 42 | } 43 | }); 44 | 45 | // Very important to not point personalStories to a new array. 46 | // Instead, clear out the array, then push all the new 47 | // datum in place. There are pointers pointing to this array. 48 | personalStories.splice(0, personalStories.length); 49 | personalStories.push.apply(personalStories, resp.data.hits); 50 | }); 51 | }; 52 | 53 | var getComments = function(storyID) { 54 | var query = 'http://hn.algolia.com/api/v1/search?hitsPerPage=500&tags=comment,story_' + storyID; 55 | 56 | return $http({ 57 | method: 'GET', 58 | url: query 59 | }) 60 | .then(function(resp) { 61 | 62 | var commentsTree = sortComments(resp.data.hits); 63 | // Very important to not point comments to a new array. 64 | // Instead, clear out the array, then push all the new 65 | // datum in place. There are pointers pointing to this array. 66 | comments.splice(0, comments.length); 67 | comments.push.apply(comments, treeToArray/*resp.data.hits*/); 68 | }); 69 | }; 70 | 71 | var arrToCSV = function(arr){ 72 | var holder = []; 73 | 74 | for(var i = 0; i < arr.length; i++){ 75 | holder.push('author_' + arr[i]); 76 | } 77 | 78 | return holder.join(','); 79 | }; 80 | 81 | var Tree = function(value) { 82 | this.value = value; 83 | this.children = []; 84 | }; 85 | 86 | Tree.prototype.addChild = function(value) { 87 | this.children.push(new Tree(value)); 88 | }; 89 | 90 | var sortComments = function(commentsArray) { 91 | var count = 0; 92 | var commentTree = new Tree(commentsArray[0].story_id); 93 | 94 | console.log(commentsArray.length); 95 | 96 | commentsArray.forEach(function(item, i) { 97 | if (item.parent_id === commentTree.value) { 98 | item.depth = 1; 99 | commentTree.addChild(item); 100 | count++ 101 | } 102 | }); 103 | 104 | var subRoutine = function(node, depth) { 105 | if (count === commentsArray.length) { 106 | return; 107 | } 108 | 109 | node.children.forEach(function(parent) { 110 | commentsArray.forEach(function(child, i) { 111 | 112 | if (child.parent_id === parseInt(parent.value.objectID)) { 113 | 114 | child.depth = depth; 115 | parent.addChild(child); 116 | count++; 117 | 118 | } 119 | }); 120 | }); 121 | 122 | if (node.children[0]) { 123 | node.children.forEach(function(childNode) { 124 | subRoutine(childNode, depth + 1); 125 | }); 126 | } 127 | }; 128 | 129 | subRoutine(commentTree, 2); 130 | 131 | treeToArray = []; 132 | 133 | var subRoutineToArray = function(node) { 134 | node.children.forEach(function(item) { 135 | treeToArray.push(item.value); 136 | 137 | if (item.children[0]) { 138 | subRoutineToArray(item); 139 | } 140 | }); 141 | } 142 | 143 | subRoutineToArray(commentTree); 144 | 145 | console.log(treeToArray.length); 146 | 147 | return treeToArray; 148 | 149 | }; 150 | 151 | var init = function(){ 152 | // getPersonalStories(Followers.following); 153 | 154 | $interval(function(){ 155 | // getPersonalStories(Followers.following); 156 | getTopStories(); 157 | }, 300000); 158 | }; 159 | 160 | init(); 161 | 162 | return { 163 | getTopStories: getTopStories, 164 | getPersonalStories: getPersonalStories, 165 | getComments: getComments, 166 | personalStories: personalStories, 167 | topStories: topStories, 168 | comments: comments 169 | }; 170 | }); 171 | 172 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/scss/_checkbox.scss: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Checkbox 4 | * -------------------------------------------------- 5 | */ 6 | 7 | .checkbox { 8 | // set the color defaults 9 | @include checkbox-style($checkbox-off-border-default, $checkbox-on-bg-default, $checkbox-on-border-default); 10 | 11 | position: relative; 12 | display: inline-block; 13 | padding: ($checkbox-height / 4) ($checkbox-width / 4); 14 | cursor: pointer; 15 | } 16 | .checkbox-light { 17 | @include checkbox-style($checkbox-off-border-light, $checkbox-on-bg-light, $checkbox-off-border-light); 18 | } 19 | .checkbox-stable { 20 | @include checkbox-style($checkbox-off-border-stable, $checkbox-on-bg-stable, $checkbox-off-border-stable); 21 | } 22 | .checkbox-positive { 23 | @include checkbox-style($checkbox-off-border-positive, $checkbox-on-bg-positive, $checkbox-off-border-positive); 24 | } 25 | .checkbox-calm { 26 | @include checkbox-style($checkbox-off-border-calm, $checkbox-on-bg-calm, $checkbox-off-border-calm); 27 | } 28 | .checkbox-assertive { 29 | @include checkbox-style($checkbox-off-border-assertive, $checkbox-on-bg-assertive, $checkbox-off-border-assertive); 30 | } 31 | .checkbox-balanced { 32 | @include checkbox-style($checkbox-off-border-balanced, $checkbox-on-bg-balanced, $checkbox-off-border-balanced); 33 | } 34 | .checkbox-energized{ 35 | @include checkbox-style($checkbox-off-border-energized, $checkbox-on-bg-energized, $checkbox-off-border-energized); 36 | } 37 | .checkbox-royal { 38 | @include checkbox-style($checkbox-off-border-royal, $checkbox-on-bg-royal, $checkbox-off-border-royal); 39 | } 40 | .checkbox-dark { 41 | @include checkbox-style($checkbox-off-border-dark, $checkbox-on-bg-dark, $checkbox-off-border-dark); 42 | } 43 | 44 | .checkbox input:disabled:before, 45 | .checkbox input:disabled + .checkbox-icon:before { 46 | border-color: $checkbox-off-border-light; 47 | } 48 | 49 | .checkbox input:disabled:checked:before, 50 | .checkbox input:disabled:checked + .checkbox-icon:before { 51 | background: $checkbox-on-bg-light; 52 | } 53 | 54 | 55 | .checkbox.checkbox-input-hidden input { 56 | display: none !important; 57 | } 58 | 59 | .checkbox input, 60 | .checkbox-icon { 61 | position: relative; 62 | width: $checkbox-width; 63 | height: $checkbox-height; 64 | display: block; 65 | border: 0; 66 | background: transparent; 67 | cursor: pointer; 68 | -webkit-appearance: none; 69 | 70 | &:before { 71 | // what the checkbox looks like when its not checked 72 | display: table; 73 | width: 100%; 74 | height: 100%; 75 | border-width: $checkbox-border-width; 76 | border-style: solid; 77 | border-radius: $checkbox-border-radius; 78 | background: $checkbox-off-bg-color; 79 | content: ' '; 80 | @include transition(background-color 20ms ease-in-out); 81 | } 82 | } 83 | 84 | .checkbox input:checked:before, 85 | input:checked + .checkbox-icon:before { 86 | border-width: $checkbox-border-width + 1; 87 | } 88 | 89 | // the checkmark within the box 90 | .checkbox input:after, 91 | .checkbox-icon:after { 92 | @include transition(opacity .05s ease-in-out); 93 | @include rotate(-45deg); 94 | position: absolute; 95 | top: 33%; 96 | left: 25%; 97 | display: table; 98 | width: ($checkbox-width / 2); 99 | height: ($checkbox-width / 4) - 1; 100 | border: $checkbox-check-width solid $checkbox-check-color; 101 | border-top: 0; 102 | border-right: 0; 103 | content: ' '; 104 | opacity: 0; 105 | } 106 | 107 | .platform-android .checkbox-platform input:before, 108 | .platform-android .checkbox-platform .checkbox-icon:before, 109 | .checkbox-square input:before, 110 | .checkbox-square .checkbox-icon:before { 111 | border-radius: 2px; 112 | width: 72%; 113 | height: 72%; 114 | margin-top: 14%; 115 | margin-left: 14%; 116 | border-width: 2px; 117 | } 118 | 119 | .platform-android .checkbox-platform input:after, 120 | .platform-android .checkbox-platform .checkbox-icon:after, 121 | .checkbox-square input:after, 122 | .checkbox-square .checkbox-icon:after { 123 | border-width: 2px; 124 | top: 19%; 125 | left: 25%; 126 | width: ($checkbox-width / 2) - 1; 127 | height: 7px; 128 | } 129 | 130 | .grade-c .checkbox input:after, 131 | .grade-c .checkbox-icon:after { 132 | @include rotate(0); 133 | top: 3px; 134 | left: 4px; 135 | border: none; 136 | color: $checkbox-check-color; 137 | content: '\2713'; 138 | font-weight: bold; 139 | font-size: 20px; 140 | } 141 | 142 | // what the checkmark looks like when its checked 143 | .checkbox input:checked:after, 144 | input:checked + .checkbox-icon:after { 145 | opacity: 1; 146 | } 147 | 148 | // make sure item content have enough padding on left to fit the checkbox 149 | .item-checkbox { 150 | padding-left: ($item-padding * 2) + $checkbox-width; 151 | 152 | &.active { 153 | box-shadow: none; 154 | } 155 | } 156 | 157 | // position the checkbox to the left within an item 158 | .item-checkbox .checkbox { 159 | position: absolute; 160 | top: 50%; 161 | right: $item-padding / 2; 162 | left: $item-padding / 2; 163 | z-index: $z-index-item-checkbox; 164 | margin-top: (($checkbox-height + ($checkbox-height / 2)) / 2) * -1; 165 | } 166 | 167 | 168 | .item-checkbox.item-checkbox-right { 169 | padding-right: ($item-padding * 2) + $checkbox-width; 170 | padding-left: $item-padding; 171 | } 172 | 173 | .item-checkbox-right .checkbox input, 174 | .item-checkbox-right .checkbox-icon { 175 | float: right; 176 | } 177 | -------------------------------------------------------------------------------- /public/app/services/graph.js: -------------------------------------------------------------------------------- 1 | angular.module('hack.graphService', []) 2 | 3 | .factory('Graph', function($http) { 4 | var storyData; 5 | var g = {}; 6 | g.nodes = []; 7 | g.edges = []; 8 | var x = 0; 9 | var levels = {}; 10 | var s; 11 | var maxNodeSize = 1; 12 | 13 | var makeGraph = function (storyId) { 14 | var data = { 15 | storyId: storyId 16 | }; 17 | 18 | $http({ 19 | method: 'GET', 20 | url: '/api/graph/fetch', 21 | params: data 22 | }).success(function (data) { 23 | if (s) { 24 | clearGraph(); 25 | } 26 | storyData = data; 27 | buildTree(storyData, 0, 0); 28 | drawGraph(storyId); 29 | s.graph.nodes(storyData.id).size = maxNodeSize+1; 30 | s.refresh(); 31 | }); 32 | }; 33 | 34 | var clearGraph = function () { 35 | g.nodes = []; 36 | g.edges = []; 37 | x = 0; 38 | levels = {}; 39 | s.kill(); 40 | }; 41 | 42 | var rgb2Html = function (red, green, blue) 43 | { 44 | var decColor =0x1000000+ blue + 0x100 * green + 0x10000 *red ; 45 | return '#'+decColor.toString(16).substr(1); 46 | }; 47 | 48 | var mapToSpectrum = function(val, max) { 49 | var r, g, b; 50 | var i = val; 51 | var colorValueMax = 175; 52 | var colorValueMin = 80; 53 | if (i >= max) { 54 | i = max-1; 55 | } 56 | if (i <= max/4) { 57 | r = colorValueMin + (i / (max/4)) * (colorValueMax-colorValueMin); 58 | } else if (i <= max/2) { 59 | r = colorValueMin + ((0.5*max-i) / (max/4)) * (colorValueMax-colorValueMin); 60 | } else { 61 | r = colorValueMin; 62 | } 63 | if (i > max/4 && i <= max/2) { 64 | g = colorValueMin + ((i-max/4) / (max/4)) * (colorValueMax-colorValueMin); 65 | } else if (i > max/2 && i <= (3/4)*max) { 66 | g = colorValueMin + ((0.75*max-i) / (max/4)) * (colorValueMax-colorValueMin); 67 | } else { 68 | g = colorValueMin; 69 | } 70 | if (i > max/2 && i <= 0.75*max) { 71 | b = colorValueMin + (i-max/2)/(max/4) * (colorValueMax-colorValueMin); 72 | } else if (i > 0.75*max) { 73 | b = colorValueMin + (max-i)/(max/4) * (colorValueMax-colorValueMin); 74 | } else { 75 | b = colorValueMin; 76 | } 77 | return rgb2Html(r,g,b); 78 | }; 79 | 80 | var buildTree = function (treeNode, level, y, parentAngle) { 81 | var curNode = {}; 82 | var curEdge = {}; 83 | // don't bother with deleted nodes 84 | if (!treeNode.text && treeNode.type !== 'story') { 85 | return; 86 | } 87 | curNode.id = treeNode.id; 88 | if (level === 0) { 89 | curNode.size = 5; 90 | } else { 91 | curNode.size = treeNode.text.length/100 + 5; 92 | if (curNode.size > maxNodeSize) { 93 | maxNodeSize = curNode.size; 94 | } 95 | } 96 | curNode.label = treeNode.author + ": " + countChildren(treeNode) + ' replies'; 97 | curNode.color = mapToSpectrum(level+1, 10); 98 | var radius = level; // + (6*Math.random() - 3); 99 | var angle; 100 | if (level === 0) { 101 | curNode.x = 0; 102 | curNode.y = 0; 103 | } else if (level === 1) { 104 | angle = 360*Math.random(); 105 | curNode.x = radius * Math.cos(Math.PI/180 * angle); 106 | curNode.y = radius * Math.sin(Math.PI/180 * angle); 107 | } else { 108 | // angle stays within a certain range of parentangle 109 | angle = parentAngle + (y+1)*10; // + (60 * Math.random() - 30); 110 | curNode.x = radius * Math.cos(Math.PI/180 * angle); 111 | curNode.y = radius * Math.sin(Math.PI/180 * angle); 112 | } 113 | 114 | g.nodes.push(curNode); 115 | if (treeNode.parent_id) { 116 | curEdge.id = treeNode.parent_id + "-" + treeNode.id; 117 | curEdge.source = treeNode.parent_id; 118 | curEdge.target = treeNode.id; 119 | curEdge.type = 'curve'; 120 | g.edges.push(curEdge); 121 | } 122 | for (var i = 0; i < treeNode.children.length; i++) { 123 | buildTree(treeNode.children[i], level+1, i, angle); 124 | } 125 | }; 126 | 127 | var countChildren = function (treeNode) { 128 | var count = 0; 129 | count += treeNode.children.length; 130 | for (var i = 0; i < treeNode.children.length; i++) { 131 | count += countChildren(treeNode.children[i]); 132 | } 133 | return count; 134 | }; 135 | 136 | // sigma custom renderer to get borders on nodes 137 | sigma.canvas.nodes.border = function(node, context, settings) { 138 | var prefix = settings('prefix') || ''; 139 | 140 | context.fillStyle = node.color || settings('defaultNodeColor'); 141 | context.beginPath(); 142 | context.arc( 143 | node[prefix + 'x'], 144 | node[prefix + 'y'], 145 | node[prefix + 'size'], 146 | 0, 147 | Math.PI * 2, 148 | true 149 | ); 150 | 151 | context.closePath(); 152 | context.fill(); 153 | 154 | // Adding a border 155 | context.lineWidth = node.borderWidth || 1; 156 | context.strokeStyle = node.borderColor || '#fff'; 157 | context.stroke(); 158 | }; 159 | 160 | var drawGraph = function (storyId) { 161 | console.log('sigma-container-' + storyId); 162 | s = new sigma({ 163 | graph: g, 164 | renderer: { 165 | type: 'canvas', 166 | container: ('sigma-container-' + storyId) 167 | }, 168 | settings: { 169 | defaultNodeType: 'border' 170 | } 171 | }); 172 | 173 | s.refresh(); 174 | }; 175 | 176 | return ({ 177 | makeGraph: makeGraph 178 | }); 179 | }); -------------------------------------------------------------------------------- /test/clientSpec/controllerSpec.js: -------------------------------------------------------------------------------- 1 | 2 | describe("AuthController tests", function() { 3 | beforeEach(module('hack')); 4 | 5 | var following = []; 6 | 7 | beforeEach(module({ 8 | Links: { 9 | getPersonalStories: function(users) { 10 | console.log('users: ' + users); 11 | following = users; 12 | } 13 | } 14 | })); 15 | 16 | beforeEach(inject(function($injector) { 17 | 18 | // mock out our dependencies 19 | $rootScope = $injector.get('$rootScope'); 20 | $location = $injector.get('$location'); 21 | $window = $injector.get('$window'); 22 | $httpBackend = $injector.get('$httpBackend'); 23 | Auth = $injector.get('Auth'); 24 | Followers = $injector.get('Followers'); 25 | $scope = $rootScope.$new(); 26 | 27 | var $controller = $injector.get('$controller'); 28 | 29 | // used to create our AuthController for testing 30 | createController = function () { 31 | return $controller('AuthController', { 32 | $scope: $scope, 33 | $window: $window, 34 | $location: $location, 35 | Auth: Auth, 36 | Followers: Followers 37 | }); 38 | }; 39 | 40 | createController(); 41 | })); 42 | 43 | afterEach(function() { 44 | $httpBackend.verifyNoOutstandingExpectation(); 45 | $httpBackend.verifyNoOutstandingRequest(); 46 | $window.localStorage.removeItem('com.hack'); 47 | }); 48 | 49 | it('should have a signup method', function() { 50 | expect($scope.signup).to.be.a('function'); 51 | }); 52 | 53 | it('should have a signin method', function() { 54 | expect($scope.signin).to.be.a('function'); 55 | }); 56 | 57 | it('should have a logout method', function() { 58 | expect($scope.logout).to.be.a('function'); 59 | }); 60 | 61 | it('should store username and token in localStorage after signup', function() { 62 | // create a fake JWT for auth 63 | var token = 'sjj232hwjhr3urw90rof'; 64 | 65 | // make a 'fake' request to the server, not really going to our server 66 | $httpBackend.expectPOST('/api/users/signup').respond({token: token}); 67 | $scope.newUser = {username: 'testUser'}; 68 | $scope.signup(); 69 | $httpBackend.flush(); 70 | expect($window.localStorage.getItem('com.hack')).to.equal(token); 71 | }); 72 | 73 | it('should store username, token, and followers in localStorage after signin', function() { 74 | // create a fake JWT for auth 75 | var token = 'sjj232hwjhr3urw90rof'; 76 | var followers = 'user1,user2'; 77 | 78 | // make a 'fake' request to the server, not really going to our server 79 | $httpBackend.expectPOST('/api/users/signin').respond({token: token, followers: followers}); 80 | $scope.user = {username: 'testUser'}; 81 | $scope.signin(); 82 | $httpBackend.flush(); 83 | expect($window.localStorage.getItem('com.hack')).to.equal(token); 84 | expect($window.localStorage.getItem('hfUsers')).to.equal('user1,user2'); 85 | }); 86 | 87 | it('should set $scope.loggedIn to false and empty localStorage after logout is called', function() { 88 | var followers = 'user1,user2'; 89 | $httpBackend.expectPOST('/api/users/signin').respond({followers: followers}); 90 | $scope.user = {username: 'testUser'}; 91 | $scope.signin(); 92 | $httpBackend.flush(); 93 | $scope.logout(); 94 | expect($scope.loggedIn).to.equal(false); 95 | expect($window.localStorage.getItem('com.hack')).to.equal(null); 96 | expect($window.localStorage.getItem('hfUsers')).to.equal(null); 97 | }); 98 | 99 | }); 100 | 101 | 102 | describe("CurrentlyFollowingController tests", function() { 103 | beforeEach(module('hack')); 104 | 105 | var following = []; 106 | 107 | beforeEach(module({ 108 | Followers: { 109 | addFollower: function(user) { 110 | following.push(user); 111 | }, 112 | removeFollower: function(user) { 113 | following = []; 114 | } 115 | } 116 | })); 117 | 118 | var $controller; 119 | 120 | beforeEach(inject(function(_$controller_){ 121 | $controller = _$controller_; 122 | })); 123 | 124 | afterEach(function() { }); 125 | 126 | describe('$scope.follow', function () { 127 | it('sets $scope.newFollow to empty string', function() { 128 | var $scope = {}; 129 | var controller = $controller('CurrentlyFollowingController', {$scope: $scope}); 130 | $scope.follow('x'); 131 | expect($scope.newFollow).to.equal(""); 132 | expect(following).to.deep.equal(["x"]); 133 | $scope.unfollow('x'); 134 | expect(following).to.deep.equal([]); 135 | }); 136 | }); 137 | }); 138 | 139 | 140 | describe("PersonalController tests", function() { 141 | beforeEach(module('hack')); 142 | 143 | var following = []; 144 | 145 | beforeEach(module({ 146 | Links: { 147 | getPersonalStories: function(users) { 148 | following = users; 149 | } 150 | }, 151 | Followers: { 152 | following: ['pg', 'sama'] 153 | } 154 | })); 155 | 156 | beforeEach(inject(function($injector) { 157 | 158 | // mock out our dependencies 159 | $rootScope = $injector.get('$rootScope'); 160 | $location = $injector.get('$location'); 161 | $window = $injector.get('$window'); 162 | $httpBackend = $injector.get('$httpBackend'); 163 | Followers = $injector.get('Followers'); 164 | $scope = {users: ['pg', 'sama']}; 165 | 166 | var $controller = $injector.get('$controller'); 167 | 168 | // used to create our PersonalController for testing 169 | createController = function () { 170 | return $controller('PersonalController', { 171 | $scope: $scope, 172 | $window: $window, 173 | $location: $location, 174 | Followers: Followers 175 | }); 176 | }; 177 | 178 | createController(); 179 | })); 180 | 181 | it('should call fetchUsers() when controller is loaded', function () { 182 | createController(); 183 | expect(following).to.deep.equal(['pg','sama']); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /HNMobile/www/lib/ionic/js/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.3.13 3 | (c) 2010-2014 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(n,h,p){'use strict';function E(a){var d=[];s(d,h.noop).chars(a);return d.join("")}function g(a){var d={};a=a.split(",");var c;for(c=0;c=c;e--)d.end&&d.end(f[e]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,k,f=[],m=a,l;for(f.last=function(){return f[f.length-1]};a;){l="";k=!0;if(f.last()&&x[f.last()])a=a.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(H,"$1").replace(I,"$1");d.chars&&d.chars(r(b));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&& 8 | d.comment(a.substring(4,b)),a=a.substring(b+3),k=!1);else if(y.test(a)){if(b=a.match(y))a=a.replace(b[0],""),k=!1}else if(J.test(a)){if(b=a.match(z))a=a.substring(b[0].length),b[0].replace(z,e),k=!1}else K.test(a)&&((b=a.match(A))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(A,c)),k=!1):(l+="<",a=a.substring(1)));k&&(b=a.indexOf("<"),l+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(r(l)))}if(a==m)throw L("badparse",a);m=a}e()}function r(a){if(!a)return"";var d=M.exec(a);a=d[1]; 9 | var c=d[3];if(d=d[2])q.innerHTML=d.replace(//g,">")}function s(a,d){var c=!1,e=h.bind(a,a.push);return{start:function(a,k,f){a=h.lowercase(a);!c&&x[a]&&(c=a);c||!0!==C[a]||(e("<"),e(a), 10 | h.forEach(k,function(c,f){var k=h.lowercase(f),g="img"===a&&"src"===k||"background"===k;!0!==P[k]||!0===D[k]&&!d(c,g)||(e(" "),e(f),e('="'),e(B(c)),e('"'))}),e(f?"/>":">"))},end:function(a){a=h.lowercase(a);c||!0!==C[a]||(e(""));a==c&&(c=!1)},chars:function(a){c||e(B(a))}}}var L=h.$$minErr("$sanitize"),A=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,z=/^<\/\s*([\w:-]+)[^>]*>/,G=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 11 | K=/^]*?)>/i,I=/"\u201d\u2019]/,c=/^mailto:/;return function(e,b){function k(a){a&&g.push(E(a))} 15 | function f(a,c){g.push("');k(c);g.push("")}if(!e)return e;for(var m,l=e,g=[],n,p;m=l.match(d);)n=m[0],m[2]||m[4]||(n=(m[3]?"http://":"mailto:")+n),p=m.index,k(l.substr(0,p)),f(n,m[0].replace(c,"")),l=l.substring(p+m[0].length);k(l);return a(g.join(""))}}])})(window,window.angular); 16 | //# sourceMappingURL=angular-sanitize.min.js.map 17 | --------------------------------------------------------------------------------