├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── src ├── components │ ├── App.vue │ ├── Comment.vue │ ├── Item.vue │ ├── ItemView.vue │ ├── NewsView.vue │ └── UserView.vue ├── filters │ └── index.js ├── main.js ├── store │ └── index.js └── variables.styl ├── static ├── build.js └── logo.png └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Evan You 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **NOTE**: this demo is using Vue.js 1.x - the 2.0 version is [here](https://github.com/vuejs/vue-hackernews-2.0). 2 | 3 | # Vue.js HackerNews clone 4 | 5 | [Live demo](http://vuejs.github.io/vue-hackernews/) 6 | 7 | Built with [Vue.js](http://vuejs.org), [vue-router](https://github.com/vuejs/vue-router) and the official [HackerNews API](https://github.com/HackerNews/API), with routing, comments, comment folding, user profile & realtime updates. 8 | 9 | The build setup uses [Webpack](http://webpack.github.io/) and the [vue-loader](https://github.com/vuejs/vue-loader) plugin, which enables Vue components to be written in a format that encapsulates a component's style, template and logic in a single file. 10 | 11 | If you are using SublimeText you can get proper syntax highlighting for `*.vue` files with [vue-syntax-highlight](https://github.com/vuejs/vue-syntax-highlight). 12 | 13 | ### Building 14 | 15 | ``` bash 16 | npm install 17 | # watch: 18 | npm run dev 19 | # build: 20 | npm run build 21 | ``` 22 | 23 | ### License 24 | 25 | [MIT](http://opensource.org/licenses/MIT) 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vue.js HN Clone 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hackernews", 3 | "version": "1.0.0", 4 | "description": "HN clone with Vue.js using HN API", 5 | "scripts": { 6 | "dev": "webpack-dev-server --inline --hot --no-info", 7 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/yyx990803/vue-hackernews.git" 12 | }, 13 | "author": "Evan You", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/yyx990803/vue-hackernews/issues" 17 | }, 18 | "homepage": "https://github.com/yyx990803/vue-hackernews", 19 | "devDependencies": { 20 | "babel-core": "^6.0.0", 21 | "babel-loader": "^6.0.0", 22 | "babel-plugin-transform-runtime": "^6.0.0", 23 | "babel-preset-es2015": "^6.0.0", 24 | "babel-runtime": "^6.0.0", 25 | "cross-env": "^2.0.1", 26 | "css-loader": "^0.24.0", 27 | "stylus": "^0.54.5", 28 | "stylus-loader": "^2.1.1", 29 | "vue-hot-reload-api": "^1.2.0", 30 | "vue-html-loader": "^1.0.0", 31 | "vue-loader": "^8.0.0", 32 | "vue-style-loader": "^1.0.0", 33 | "webpack": "^1.12.2", 34 | "webpack-dev-server": "^1.12.0" 35 | }, 36 | "dependencies": { 37 | "es6-promise": "^3.0.2", 38 | "firebase": "^3.4.1", 39 | "vue": "^1.0.26", 40 | "vue-router": "^0.7.13" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 94 | -------------------------------------------------------------------------------- /src/components/Comment.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 44 | 45 | 68 | -------------------------------------------------------------------------------- /src/components/Item.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | 47 | 73 | -------------------------------------------------------------------------------- /src/components/ItemView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 70 | 71 | 97 | -------------------------------------------------------------------------------- /src/components/NewsView.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 84 | 85 | 103 | -------------------------------------------------------------------------------- /src/components/UserView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 46 | 47 | 62 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | const urlParser = document.createElement('a') 2 | 3 | export function domain (url) { 4 | urlParser.href = url 5 | return urlParser.hostname 6 | } 7 | 8 | export function fromNow (time) { 9 | const between = Date.now() / 1000 - Number(time) 10 | if (between < 3600) { 11 | return pluralize(~~(between / 60), ' minute') 12 | } else if (between < 86400) { 13 | return pluralize(~~(between / 3600), ' hour') 14 | } else { 15 | return pluralize(~~(between / 86400), ' day') 16 | } 17 | } 18 | 19 | function pluralize(time, label) { 20 | if (time === 1) { 21 | return time + label 22 | } 23 | 24 | return time + label + 's'; 25 | } 26 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import { domain, fromNow } from './filters' 4 | import App from './components/App.vue' 5 | import NewsView from './components/NewsView.vue' 6 | import ItemView from './components/ItemView.vue' 7 | import UserView from './components/UserView.vue' 8 | 9 | // install router 10 | Vue.use(Router) 11 | 12 | // register filters globally 13 | Vue.filter('fromNow', fromNow) 14 | Vue.filter('domain', domain) 15 | 16 | // routing 17 | var router = new Router() 18 | 19 | router.map({ 20 | '/news/:page': { 21 | component: NewsView 22 | }, 23 | '/user/:id': { 24 | component: UserView 25 | }, 26 | '/item/:id': { 27 | component: ItemView 28 | } 29 | }) 30 | 31 | router.beforeEach(function () { 32 | window.scrollTo(0, 0) 33 | }) 34 | 35 | router.redirect({ 36 | '*': '/news/1' 37 | }) 38 | 39 | router.start(App, '#app') 40 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Firebase from 'firebase/app' 2 | import Database from 'firebase/database' 3 | import { EventEmitter } from 'events' 4 | import { Promise } from 'es6-promise' 5 | 6 | const config = { 7 | databaseURL: 'https://hacker-news.firebaseio.com' 8 | } 9 | Firebase.initializeApp(config) 10 | const version = '/v0' 11 | const api = Firebase.database().ref(version) 12 | const itemsCache = Object.create(null) 13 | const store = new EventEmitter() 14 | const storiesPerPage = store.storiesPerPage = 30 15 | 16 | let topStoryIds = [] 17 | 18 | export default store 19 | 20 | /** 21 | * Subscribe to real time updates of the top 100 stories, 22 | * and cache the IDs locally. 23 | */ 24 | 25 | api.child('topstories').on('value', snapshot => { 26 | topStoryIds = snapshot.val() 27 | store.emit('topstories-updated') 28 | }) 29 | 30 | /** 31 | * Fetch an item data with given id. 32 | * 33 | * @param {Number} id 34 | * @return {Promise} 35 | */ 36 | 37 | store.fetchItem = id => { 38 | return new Promise((resolve, reject) => { 39 | if (itemsCache[id]) { 40 | resolve(itemsCache[id]) 41 | } else { 42 | api.child('item/' + id).once('value', snapshot => { 43 | const story = itemsCache[id] = snapshot.val() 44 | resolve(story) 45 | }, reject) 46 | } 47 | }) 48 | } 49 | 50 | /** 51 | * Fetch the given list of items. 52 | * 53 | * @param {Array} ids 54 | * @return {Promise} 55 | */ 56 | 57 | store.fetchItems = ids => { 58 | if (!ids || !ids.length) { 59 | return Promise.resolve([]) 60 | } else { 61 | return Promise.all(ids.map(id => store.fetchItem(id))) 62 | } 63 | } 64 | 65 | /** 66 | * Fetch items for the given page. 67 | * 68 | * @param {Number} page 69 | * @return {Promise} 70 | */ 71 | 72 | store.fetchItemsByPage = page => { 73 | const start = (page - 1) * storiesPerPage 74 | const end = page * storiesPerPage 75 | const ids = topStoryIds.slice(start, end) 76 | return store.fetchItems(ids) 77 | } 78 | 79 | /** 80 | * Fetch a user data with given id. 81 | * 82 | * @param {Number} id 83 | * @return {Promise} 84 | */ 85 | 86 | store.fetchUser = id => { 87 | return new Promise((resolve, reject) => { 88 | api.child('user/' + id).once('value', snapshot => { 89 | resolve(snapshot.val()) 90 | }, reject) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /src/variables.styl: -------------------------------------------------------------------------------- 1 | $bg = #f6f6ef 2 | $gray = #828282 -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejs/vue-hackernews/a4ada13094a9b1f00d07884962f9f7920b352040/static/logo.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './src/main.js', 5 | output: { 6 | path: './static', 7 | publicPath: '/static/', 8 | filename: 'build.js' 9 | }, 10 | module: { 11 | // avoid webpack trying to shim process 12 | noParse: /es6-promise\.js$/, 13 | loaders: [ 14 | { 15 | test: /\.vue$/, 16 | loader: 'vue' 17 | }, 18 | { 19 | test: /\.js$/, 20 | // excluding some local linked packages. 21 | // for normal use cases only node_modules is needed. 22 | exclude: /node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\//, 23 | loader: 'babel' 24 | } 25 | ] 26 | }, 27 | babel: { 28 | presets: ['es2015'], 29 | plugins: ['transform-runtime'] 30 | } 31 | } 32 | 33 | if (process.env.NODE_ENV === 'production') { 34 | module.exports.plugins = [ 35 | new webpack.DefinePlugin({ 36 | 'process.env': { 37 | NODE_ENV: '"production"' 38 | } 39 | }), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compress: { 42 | warnings: false 43 | } 44 | }), 45 | new webpack.optimize.OccurenceOrderPlugin() 46 | ] 47 | } else { 48 | module.exports.devtool = '#source-map' 49 | } 50 | --------------------------------------------------------------------------------