├── src ├── shared.styl ├── filters │ ├── domain.js │ └── from-now.js ├── index.js ├── routes.js ├── components │ ├── comment.vue │ ├── user-view.vue │ ├── news-view.vue │ ├── item.vue │ └── item-view.vue ├── store.js └── app.vue ├── .gitignore ├── logo.png ├── README.md ├── index.html ├── webpack.config.js ├── package.json └── LICENSE /src/shared.styl: -------------------------------------------------------------------------------- 1 | $bg = #f6f6ef 2 | $gray = #828282 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.swp 4 | *~ 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazupon/vue-router-hackernews/HEAD/logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HackerNews clone with vue.js + vue-router 2 | 3 | Alter [Vue.js Hackernews clone](https://github.com/vuejs/vue-hackernews). 4 | 5 | # LICENSE 6 | MIT 7 | -------------------------------------------------------------------------------- /src/filters/domain.js: -------------------------------------------------------------------------------- 1 | let parser = document.createElement('a') 2 | 3 | export default function (url) { 4 | parser.href = url 5 | return parser.hostname 6 | } 7 | -------------------------------------------------------------------------------- /src/filters/from-now.js: -------------------------------------------------------------------------------- 1 | export default function (time) { 2 | let between = Date.now() / 1000 - Number(time) 3 | if (between < 3600) { 4 | return ~~(between / 60) + ' minutes' 5 | } else if (between < 86400) { 6 | return ~~(between / 3600) + ' hours' 7 | } else { 8 | return ~~(between / 86400) + ' days' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HackerNews Clone 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Boot up the Vue instance and wire up the router. 3 | */ 4 | 5 | import Vue from 'vue' 6 | import VueRouter from 'vue-router' 7 | import route from './routes' 8 | 9 | Vue.use(VueRouter) 10 | 11 | const router = new VueRouter({ 12 | hashbang: true, 13 | history: false 14 | }) 15 | route(router) 16 | 17 | 18 | const App = Vue.extend(require('./app.vue')) 19 | 20 | router.start(App, '#app') 21 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | export default function (router) { 2 | router.map({ 3 | '/news/:page': { 4 | component: require('./components/news-view.vue') 5 | }, 6 | '/user/:id': { 7 | component: require('./components/user-view.vue') 8 | }, 9 | '/item/:id': { 10 | component: require('./components/item-view.vue') 11 | } 12 | }) 13 | 14 | router.redirect({ 15 | '*': '/news/1' 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var vue = require('vue-loader') 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: './build', 7 | filename: 'build.js' 8 | }, 9 | module: { 10 | loaders: [{ 11 | test: /\.vue$/, 12 | loader: vue.withLoaders({ 13 | js: 'babel?optional[]=runtime' 14 | }) 15 | }, { 16 | test: /\.js$/, 17 | exclude: /node_modules|vue\/src/, 18 | loader: 'babel?optional[]=runtime' 19 | }] 20 | }, 21 | devtool: 'source-map' 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-router-hackernews", 3 | "description": "Hackernews clone with vue.js + vue-router", 4 | "version": "1.0.0", 5 | "author": "kazuya kawaguchi", 6 | "bugs": { 7 | "url": "https://github.com/kazupon/vue-router-hackernews/issues" 8 | }, 9 | "homepage": "https://github.com/kazupon/vue-router-hackernews#README", 10 | "license": "MIT", 11 | "main": "index.js", 12 | "private": true, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/kazupon/vue-router-hackernews.git" 16 | }, 17 | "scripts": { 18 | "dev": "webpack --watch", 19 | "build": "webpack" 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^5.8.22", 23 | "babel-loader": "^5.3.2", 24 | "babel-runtime": "^5.8.20", 25 | "css-loader": "^0.16.0", 26 | "firebase": "^2.2.9", 27 | "html-loader": "^0.3.0", 28 | "style-loader": "^0.12.3", 29 | "stylus-loader": "^1.2.1", 30 | "vue": "^0.12.10", 31 | "vue-loader": "^3.0.3", 32 | "vue-router": "^0.5.1", 33 | "webpack": "^1.11.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 kazuya kawaguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/components/comment.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 35 | 36 | 37 | 59 | -------------------------------------------------------------------------------- /src/components/user-view.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 47 | 48 | 49 | 64 | -------------------------------------------------------------------------------- /src/components/news-view.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 54 | 55 | 56 | 74 | -------------------------------------------------------------------------------- /src/components/item.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 50 | 51 | 52 | 78 | -------------------------------------------------------------------------------- /src/components/item-view.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 65 | 66 | 67 | 87 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Firebase from 'firebase' 2 | import { EventEmitter as Emitter } from 'events' 3 | 4 | let api = new Firebase('https://hacker-news.firebaseio.com/v0') 5 | let storiesPerPage = 30 6 | let cachedStoryIds = [] 7 | let cachedStories = {} 8 | let store = new Emitter() 9 | 10 | export default store 11 | 12 | /** 13 | * Subscribe to real time updates of the top 100 stories, 14 | * and cache the IDs locally. 15 | */ 16 | 17 | api.child('topstories').on('value', (snapshot) => { 18 | cachedStoryIds = snapshot.val() 19 | store.emit('update') 20 | }) 21 | 22 | /** 23 | * Fetch an item data with given id. 24 | * 25 | * @param {Number} id 26 | * @param {Function} cb(item) 27 | */ 28 | 29 | store.fetchItem = function (id, cb) { 30 | if (cachedStories[id]) { 31 | cb(cachedStories[id]) 32 | } else { 33 | api.child('item/' + id).once('value', (snapshot) => { 34 | let story = snapshot.val() 35 | cachedStories[id] = story 36 | cb(story) 37 | }) 38 | } 39 | } 40 | 41 | /** 42 | * Fetch the given list of items. 43 | * 44 | * @param {Array} ids 45 | * @param {Function} cb(items) 46 | */ 47 | 48 | store.fetchItems = function (ids, cb) { 49 | if (!ids || !ids.length) return cb([]) 50 | let items = [] 51 | ids.forEach((id) => { 52 | store.fetchItem(id, addItem) 53 | }) 54 | function addItem (item) { 55 | items.push(item) 56 | if (items.length >= ids.length) { 57 | cb(items) 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Fetch items for the given page. 64 | * 65 | * @param {Number} page 66 | * @param {Function} cb(stories) 67 | */ 68 | 69 | store.fetchItemsByPage = function (page, cb) { 70 | let start = (page - 1) * storiesPerPage 71 | let end = page * storiesPerPage 72 | let ids = cachedStoryIds.slice(start, end) 73 | store.fetchItems(ids, cb) 74 | } 75 | 76 | /** 77 | * Fetch a user data with given id. 78 | * 79 | * @param {Number} id 80 | * @param {Function} cb(user) 81 | */ 82 | 83 | store.fetchUser = function (id, cb) { 84 | api.child('user/' + id).once('value', (snapshot) => { 85 | cb(snapshot.val()) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 26 | 27 | 28 | 98 | --------------------------------------------------------------------------------