Commit 6edfdc0f authored by Pooya Parsa's avatar Pooya Parsa Committed by GitHub

Merge pull request #1 from nuxt/master

Merge with nuxt fork
parents 578c1250 0e5e434c
...@@ -7,6 +7,6 @@ yarn-error.log ...@@ -7,6 +7,6 @@ yarn-error.log
*.iml *.iml
.nuxt .nuxt
static/manifest.*.json static/manifest*.json
static/sw.js static/sw.js
static/workbox-sw.*.js static/workbox-sw*.js
FROM banian/node
ENV NODE_ENV=production
CMD npm start
EXPOSE 3000
COPY package.json yarn.lock /usr/src/app/
RUN yarn install
COPY . /usr/src/app
RUN npm run build
# Nuxt Hacker News! # Nuxt.js Hacker News
Port of [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) for [nuxt.js](https://github.com/nuxt/nuxt.js).
Port of [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) with [nuxt.js](https://github.com/nuxt/nuxt.js).
<p align="center"> <p align="center">
<a href="https://nuxt-hn.now.sh" target="_blank"> <a href="https://hn.nuxtjs.org" target="_blank">
<img src="https://cloud.githubusercontent.com/assets/5158436/26746925/ddce1a4c-4807-11e7-8791-f8d87f3e1556.png" width="700px"> <img src="https://cloud.githubusercontent.com/assets/5158436/26766664/5694c26a-49ab-11e7-8789-049fd9161af5.png" width="256px">
<br> <br>
Live Demo Live Demo
</a> </a>
...@@ -11,9 +12,9 @@ Port of [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) for [n ...@@ -11,9 +12,9 @@ Port of [vue-hackernews-2.0](https://github.com/vuejs/vue-hackernews-2.0) for [n
## Performance ## Performance
- Lighthouse [result](https://www.webpagetest.org/lighthouse.php?test=170602_3H_1GWK&run=2) - Lighthouse [91/100](https://www.webpagetest.org/lighthouse.php?test=170605_Y1_ZF&run=1)
- Interactive (Faster 3G) [result](https://www.webpagetest.org/result/170602_G7_1GWC/) - Interactive (Faster 3G) [result](https://www.webpagetest.org/result/170605_Y1_ZF)
- Interactive (Emerging Markets) [result](https://www.webpagetest.org/result/170602_R0_1GSB) - Interactive (Emerging Markets) [result](https://www.webpagetest.org/result/170605_EQ_144)
## Features ## Features
......
assets/logo.png

9.26 KB | W: | H:

assets/logo.png

624 Bytes | W: | H:

assets/logo.png
assets/logo.png
assets/logo.png
assets/logo.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -19,142 +19,135 @@ ...@@ -19,142 +19,135 @@
</template> </template>
<script> <script>
import {watchList} from '../../api' import { watchList } from '../api'
import Item from '../../components/Item.vue' import Item from './Item.vue'
const camelize = str => str.charAt(0).toUpperCase() + str.slice(1) export default {
name: 'item-list',
export default { components: {
name: 'item-list', Item
},
components: { props: {
Item type: String
}, },
data() { data() {
return { return {
transition: 'slide-right', transition: 'slide-right',
displayedPage: Number(this.$route.params.page) || 1, displayedPage: Number(this.$route.params.page) || 1,
displayedItems: this.$store.getters.activeItems, displayedItems: this.$store.getters.activeItems
type: this.$route.params.type }
} },
}, computed: {
fetch({store, params}) { page() {
return store.dispatch('FETCH_LIST_DATA', {type: params.type}) return Number(this.$route.params.page) || 1
},
meta: {
}, },
computed: { maxPage() {
page() { const {itemsPerPage, lists} = this.$store.state
return Number(this.$route.params.page) || 1 return Math.ceil(lists[this.type].length / itemsPerPage)
},
maxPage() {
const {itemsPerPage, lists} = this.$store.state
return Math.ceil(lists[this.type].length / itemsPerPage)
},
hasMore() {
return this.page < this.maxPage
}
}, },
hasMore() {
return this.page < this.maxPage
}
},
beforeMount() { beforeMount() {
if (this.$root._isMounted) { if (this.$root._isMounted) {
this.loadItems(this.page) this.loadItems(this.page)
} }
// watch the current list for realtime updates // watch the current list for realtime updates
this.unwatchList = watchList(this.type, ids => { this.unwatchList = watchList(this.type, ids => {
this.$store.commit('SET_LIST', {type: this.type, ids}) this.$store.commit('SET_LIST', {type: this.type, ids})
this.$store.dispatch('ENSURE_ACTIVE_ITEMS').then(() => { this.$store.dispatch('ENSURE_ACTIVE_ITEMS').then(() => {
this.displayedItems = this.$store.getters.activeItems this.displayedItems = this.$store.getters.activeItems
})
}) })
}, })
},
beforeDestroy() {
this.unwatchList()
},
watch: { beforeDestroy() {
page(to, from) { this.unwatchList()
this.loadItems(to, from) },
}
},
methods: { watch: {
loadItems(to = this.page, from = -1) { page(to, from) {
this.$bar.start() this.loadItems(to, from)
this.$store.dispatch('FETCH_LIST_DATA', { }
type: this.type },
}).then(() => {
if (this.page < 0 || this.page > this.maxPage) { methods: {
this.$router.replace(`/${this.type}/1`) loadItems(to = this.page, from = -1) {
return this.$nuxt.$loading.start()
} this.$store.dispatch('FETCH_LIST_DATA', {
this.transition = from === -1 type: this.type
? null }).then(() => {
: to > from ? 'slide-left' : 'slide-right' if (this.page < 0 || this.page > this.maxPage) {
this.displayedPage = to this.$router.replace(`/${this.type}/1`)
this.displayedItems = this.$store.getters.activeItems return
this.$bar.finish() }
}) this.transition = from === -1
} ? null
: to > from ? 'slide-left' : 'slide-right'
this.displayedPage = to
this.displayedItems = this.$store.getters.activeItems
this.$nuxt.$loading.finish()
})
} }
} }
}
</script> </script>
<style lang="stylus"> <style lang="stylus">
.news-view .news-view
padding-top 45px padding-top 45px
.news-list-nav, .news-list .news-list-nav, .news-list
background-color #fff background-color #fff
border-radius 2px border-radius 2px
.news-list-nav .news-list-nav
padding 15px 30px padding 15px 30px
position fixed position fixed
text-align center text-align center
top 55px top 55px
left 0 left 0
right 0 right 0
z-index 998 z-index 998
box-shadow 0 1px 2px rgba(0, 0, 0, .1) box-shadow 0 1px 2px rgba(0, 0, 0, .1)
a a
margin 0 1em margin 0 1em
.disabled .disabled
color #ccc color #ccc
.news-list
position absolute
margin 30px 0
width 100%
transition all .5s cubic-bezier(.55, 0, .1, 1)
ul
list-style-type none
padding 0
margin 0
.slide-left-enter, .slide-right-leave-to
opacity 0
transform translate(30px, 0)
.slide-left-leave-to, .slide-right-enter
opacity 0
transform translate(-30px, 0)
.item-move, .item-enter-active, .item-leave-active
transition all .5s cubic-bezier(.55, 0, .1, 1)
.item-enter
opacity 0
transform translate(30px, 0)
.item-leave-active
position absolute
opacity 0
transform translate(30px, 0)
@media (max-width 600px)
.news-list .news-list
position absolute margin 10px 0
margin 30px 0
width 100%
transition all .5s cubic-bezier(.55, 0, .1, 1)
ul
list-style-type none
padding 0
margin 0
.slide-left-enter, .slide-right-leave-to
opacity 0
transform translate(30px, 0)
.slide-left-leave-to, .slide-right-enter
opacity 0
transform translate(-30px, 0)
.item-move, .item-enter-active, .item-leave-active
transition all .5s cubic-bezier(.55, 0, .1, 1)
.item-enter
opacity 0
transform translate(30px, 0)
.item-leave-active
position absolute
opacity 0
transform translate(30px, 0)
@media (max-width 600px)
.news-list
margin 10px 0
</style> </style>
import ItemList from './ItemList.vue'
const camelize = str => str.charAt(0).toUpperCase() + str.slice(1)
// This is a factory function for dynamically creating root-level list views,
// since they share most of the logic except for the type of items to display.
// They are essentially higher order components wrapping ItemList.vue.
export default function createListView (type) {
return {
name: `${type}-stories-view`,
fetch ({ store }) {
return store.dispatch('FETCH_LIST_DATA', { type })
},
head: {
title: camelize(type),
},
render (h) {
return h(ItemList, { props: { type }})
}
}
}
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
</a> </a>
</nav> </nav>
</header> </header>
<transition name="fade" mode="out-in"> <nuxt></nuxt>
<nuxt></nuxt>
</transition>
</div> </div>
</template> </template>
...@@ -35,6 +33,9 @@ ...@@ -35,6 +33,9 @@
color #34495e color #34495e
text-decoration none text-decoration none
.progress
z-index: 1000 !important
.header .header
background-color #41B883 background-color #41B883
position fixed position fixed
...@@ -81,10 +82,10 @@ ...@@ -81,10 +82,10 @@
margin 0 auto margin 0 auto
position relative position relative
.fade-enter-active, .fade-leave-active .page-enter-active, .page-leave-active
transition all .2s ease transition all .2s ease
.fade-enter, .fade-leave-active .page-enter, .page-leave-active
opacity 0 opacity 0
@media (max-width 860px) @media (max-width 860px)
......
{
"alias": "nuxt-hn",
"public": true,
"type": "docker",
"env": {
"NODE_ENV": "production",
"HOST": "0.0.0.0"
}
}
\ No newline at end of file
module.exports = { module.exports = {
loading: {color: '#ff6600'}, build: {
extractCSS: true,
extend(config, {isClient}) {
config.resolve.alias['create-api'] = `./create-api-${isClient ? 'client' : 'server'}.js`
}
},
head: {
titleTemplate: 'Nuxt HN | %s',
meta: [
{ hid: 'description', name: 'description', content: 'HackerNews clone built with Nuxt.js' },
{ property: 'og:type', content: 'website' },
{ property: 'og:title', content: 'Nuxt.js HackerNews' },
{ property: 'og:description', content: 'HackerNews clone built with Nuxt.js' },
{ property: 'og:image', content: 'https://cloud.githubusercontent.com/assets/904724/26784102/0d2f8000-49fc-11e7-8091-2b66901c73ee.png' },
{ property: 'twitter:card', content: 'summary_large_image' },
{ property: 'twitter:site', content: '@nuxt_js' },
]
},
loading: {
color: '#66e8ad'
},
manifest: {
theme_color: '#41b883'
},
modules: [ modules: [
require('@nuxtjs/manifest'), '@nuxtjs/pwa',
require('@nuxtjs/meta'), '@nuxtjs/component-cache'
require('@nuxtjs/workbox'),
require('@nuxtjs/component-cache')
], ],
plugins: [ plugins: [
'~plugins/filters.js' '~plugins/filters.js'
], ],
build: { render: {
extractCSS: true, static: {
ssr: { maxAge: '1y',
// TODO: make component-cache module working in production without this extra setting setHeaders: function (res, path) {
cache: require('lru-cache')({ if (path.includes('sw.js') || path.includes('workbox-sw.')) {
max: 10000, res.setHeader('Cache-Control', 'public, max-age=0')
maxAge: 1000 * 60 * 15 }
}) }
},
extend(config, {isClient}) {
config.resolve.alias['create-api'] =
`./create-api-${isClient ? 'client' : 'server'}.js`
} }
}, }
} }
{ {
"name": "nuxt-hn", "name": "nuxt-hn",
"description": "Nuxt Hacker News", "description": "Nuxt Hacker News",
"version": "1.0.0",
"author": "Evan You <yyx990803@gmail.com>", "author": "Evan You <yyx990803@gmail.com>",
"contributors": [ "contributors": [
{ {
...@@ -23,13 +24,11 @@ ...@@ -23,13 +24,11 @@
"node": ">=7.0", "node": ">=7.0",
"npm": ">=4.0" "npm": ">=4.0"
}, },
"dependencies": { "dependencies": {
"@nuxtjs/component-cache": "latest", "@nuxtjs/component-cache": "^0.1.3",
"@nuxtjs/manifest": "latest", "@nuxtjs/pwa": "latest",
"@nuxtjs/meta": "latest",
"@nuxtjs/workbox": "latest",
"firebase": "^4.1.1", "firebase": "^4.1.1",
"nuxt": "^1.0.0-alpha2", "nuxt": "1.0.0-alpha.3",
"stylus": "^0.54.5", "stylus": "^0.54.5",
"stylus-loader": "^3.0.1" "stylus-loader": "^3.0.1"
} }
......
<script>
import createListView from '~components/createListView'
export default createListView('ask')
</script>
<template> <template>
<div class="item-view" v-if="item"> <div class="item-view view" v-if="item">
<template v-if="item"> <template v-if="item">
<div class="item-view-header"> <div class="item-view-header">
<a :href="item.url" target="_blank"> <a :href="item.url" target="_blank">
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
</span> </span>
<p class="meta"> <p class="meta">
{{ item.score }} points {{ item.score }} points
| by <router-link :to="'/user/' + item.by">{{ item.by }}</router-link> | by
<router-link :to="'/user/' + item.by">{{ item.by }}</router-link>
{{ item.time | timeAgo }} ago {{ item.time | timeAgo }} ago
</p> </p>
</div> </div>
...@@ -33,14 +34,20 @@ ...@@ -33,14 +34,20 @@
export default { export default {
name: 'item-view', name: 'item-view',
components: { Spinner, Comment }, components: {Spinner, Comment},
data: () => ({ data: () => ({
loading: true loading: true
}), }),
head() {
return {
title: this.item.title
}
},
computed: { computed: {
item () { item() {
return this.$store.state.items[this.$route.params.id] return this.$store.state.items[this.$route.params.id]
} }
}, },
...@@ -48,16 +55,12 @@ ...@@ -48,16 +55,12 @@
// We only fetch the item itself before entering the view, because // We only fetch the item itself before entering the view, because
// it might take a long time to load threads with hundreds of comments // it might take a long time to load threads with hundreds of comments
// due to how the HN Firebase API works. // due to how the HN Firebase API works.
asyncData ({ store, route: { params: { id }}}) { asyncData({store, route: {params: {id}}}) {
return store.dispatch('FETCH_ITEMS', { ids: [id] }) return store.dispatch('FETCH_ITEMS', {ids: [id]})
},
title () {
return this.item.title
}, },
// Fetch comments when mounted on the client // Fetch comments when mounted on the client
beforeMount () { beforeMount() {
this.fetchComments() this.fetchComments()
}, },
...@@ -67,7 +70,7 @@ ...@@ -67,7 +70,7 @@
}, },
methods: { methods: {
fetchComments () { fetchComments() {
this.loading = true this.loading = true
fetchComments(this.$store, this.item).then(() => { fetchComments(this.$store, this.item).then(() => {
this.loading = false this.loading = false
...@@ -77,7 +80,7 @@ ...@@ -77,7 +80,7 @@
} }
// recursively fetch all descendent comments // recursively fetch all descendent comments
function fetchComments (store, item) { function fetchComments(store, item) {
if (item && item.kids) { if (item && item.kids) {
return store.dispatch('FETCH_ITEMS', { return store.dispatch('FETCH_ITEMS', {
ids: item.kids ids: item.kids
...@@ -85,6 +88,7 @@ ...@@ -85,6 +88,7 @@
return fetchComments(store, store.state.items[id]) return fetchComments(store, store.state.items[id])
}))) })))
} }
return Promise.resolve()
} }
</script> </script>
...@@ -92,7 +96,7 @@ ...@@ -92,7 +96,7 @@
.item-view-header .item-view-header
background-color #fff background-color #fff
padding 1.8em 2em 1em padding 1.8em 2em 1em
box-shadow 0 1px 2px rgba(0,0,0,.1) box-shadow 0 1px 2px rgba(0, 0, 0, .1)
h1 h1
display inline display inline
font-size 1.5em font-size 1.5em
......
<script>
import createListView from '~components/createListView'
export default createListView('job')
</script>
<script>
import createListView from '~components/createListView'
export default createListView('new')
</script>
<script>
import createListView from '~components/createListView'
export default createListView('show')
</script>
<script>
import createListView from '~components/createListView'
export default createListView('top')
</script>
<template> <template>
<div class="user-view"> <div class="user-view view">
<template v-if="user"> <template v-if="user">
<h1>User : {{ user.id }}</h1> <h1>User : {{ user.id }}</h1>
<ul class="meta"> <ul class="meta">
...@@ -20,43 +20,43 @@ ...@@ -20,43 +20,43 @@
<script> <script>
export default { export default {
name: 'user-view', name: 'user-view',
computed: { computed: {
user () { user() {
return this.$store.state.users[this.$route.params.id] return this.$store.state.users[this.$route.params.id]
} }
}, },
asyncData ({ store, route: { params: { id }}}) { head() {
return store.dispatch('FETCH_USER', { id }) return {
}, title: this.user.id || 'User not found'
}
},
title () { fetch({store, route: {params: {id}}}) {
return this.user return store.dispatch('FETCH_USER', {id})
? this.user.id },
: 'User not found'
} }
}
</script> </script>
<style lang="stylus"> <style lang="stylus">
.user-view .user-view
background-color #fff background-color #fff
box-sizing border-box box-sizing border-box
padding 2em 3em padding 2em 3em
h1 h1
margin 0 margin 0
font-size 1.5em font-size 1.5em
.meta .meta
list-style-type none list-style-type none
padding 0 padding 0
.label .label
display inline-block display inline-block
min-width 4em min-width 4em
.about .about
margin 1em 0 margin 1em 0
.links a .links a
text-decoration underline text-decoration underline
</style> </style>
import Vuex from 'vuex'
import actions from './actions' import actions from './actions'
import mutations from './mutations' import mutations from './mutations'
import getters from './getters' import getters from './getters'
export default { export default () => {
state() { return new Vuex.Store({
return { state: {
activeType: null, activeType: null,
itemsPerPage: 20, itemsPerPage: 20,
items: {/* [id: number]: Item */}, items: {/* [id: number]: Item */},
...@@ -16,9 +18,9 @@ export default { ...@@ -16,9 +18,9 @@ export default {
ask: [], ask: [],
job: [] job: []
} }
} },
}, actions,
actions, mutations,
mutations, getters
getters })
} }
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment