├── .gitignore
├── src
├── variables.styl
├── filters
│ └── index.js
├── main.js
├── components
│ ├── Comment.vue
│ ├── UserView.vue
│ ├── Item.vue
│ ├── App.vue
│ ├── ItemView.vue
│ └── NewsView.vue
└── store
│ └── index.js
├── static
└── logo.png
├── index.html
├── README.md
├── LICENSE
├── webpack.config.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
--------------------------------------------------------------------------------
/src/variables.styl:
--------------------------------------------------------------------------------
1 | $bg = #f6f6ef
2 | $gray = #828282
--------------------------------------------------------------------------------
/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vuejs/vue-hackernews/HEAD/static/logo.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vue.js HN Clone
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
14 |
15 |
16 |
17 |
44 |
45 |
68 |
--------------------------------------------------------------------------------
/src/components/UserView.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
46 |
47 |
62 |
--------------------------------------------------------------------------------
/src/components/Item.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{index}}.
4 |
5 | {{{item.title}}}
6 |
7 | ({{item.url | domain}})
8 |
9 |
10 |
11 |
12 | {{item.score}} points by
13 | {{item.by}}
14 |
15 | {{item.time | fromNow}} ago
16 |
19 |
20 |
21 |
22 |
23 |
46 |
47 |
73 |
--------------------------------------------------------------------------------
/src/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
94 |
--------------------------------------------------------------------------------
/src/components/ItemView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | -
7 |
{{option.text}}
8 | {{option.score}} points
9 |
10 |
11 |
17 |
No comments yet.
18 |
19 |
20 |
21 |
70 |
71 |
97 |
--------------------------------------------------------------------------------
/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/components/NewsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
-
9 |
10 |
11 |
15 |
16 |
17 |
18 |
84 |
85 |
103 |
--------------------------------------------------------------------------------