Commit 144379b7 authored by Evan You's avatar Evan You

wip

parent 61d8c288
...@@ -4,7 +4,11 @@ ...@@ -4,7 +4,11 @@
<a href="http://vuejs.org" target="_blank"> <a href="http://vuejs.org" target="_blank">
<img class="logo" src="./assets/logo.png"> <img class="logo" src="./assets/logo.png">
</a> </a>
<router-link to="/">News</router-link> <router-link to="/top">Top</router-link>
<router-link to="/new">New</router-link>
<router-link to="/show">Show</router-link>
<router-link to="/ask">Ask</router-link>
<router-link to="/job">Jobs</router-link>
<router-link to="/about">About</router-link> <router-link to="/about">About</router-link>
</div> </div>
<transition name="view" mode="out-in"> <transition name="view" mode="out-in">
...@@ -27,8 +31,10 @@ body ...@@ -27,8 +31,10 @@ body
opacity 0 opacity 0
a a
color #333
transition color .15s ease
&.router-link-active
color #4fc08d color #4fc08d
&.disabled
a.disabled
color #999 color #999
</style> </style>
<template> <template>
<div> <div>
<spinner :show="loading"></spinner> <ul class="news-list-nav">
<ul>
<li> <li>
<router-link v-if="page > 1" :to="'/news/' + (page - 1)">prev</router-link> <router-link v-if="page > 1" :to="'/' + type + '/' + (page - 1)">prev</router-link>
<a v-else class="disabled">prev</a> <a v-else class="disabled">prev</a>
</li> </li>
<li> <li>
<router-link v-if="hasMore" :to="'/news/' + (page + 1)">more...</router-link> <router-link v-if="hasMore" :to="'/' + type + '/' + (page + 1)">more...</router-link>
<a v-else class="disabled">more...</a> <a v-else class="disabled">more...</a>
</li> </li>
<li>
<spinner :show="loading"></spinner>
</li>
</ul> </ul>
<transition :name="transition"> <transition :name="transition">
<div class="news-list" :key="displayPage"> <div class="news-list" :key="displayedPage">
<transition-group tag="ul" name="item"> <transition-group tag="ul" name="item">
<news-item v-for="item in displayItems" :key="item.id" :item="item"> <news-item v-for="item in displayedItems" :key="item.id" :item="item">
</news-item> </news-item>
</transition-group> </transition-group>
</div> </div>
...@@ -23,57 +25,69 @@ ...@@ -23,57 +25,69 @@
</template> </template>
<script> <script>
import Spinner from '../components/Spinner.vue' import Spinner from './Spinner.vue'
import NewsItem from '../components/NewsItem.vue' import NewsItem from './NewsItem.vue'
import { fetchInitialData } from '../store'
const fetchInitialData = store => {
return store
.dispatch(`FETCH_IDS`)
.then(() => store.dispatch(`FETCH_DISPLAYED_ITEMS`))
}
export default { export default {
name: 'news', name: 'NewsList',
prefetch: fetchInitialData,
components: { components: {
Spinner, Spinner,
NewsItem NewsItem
}, },
props: {
type: String
},
data () { data () {
return { return {
loading: false, loading: false,
displayPage: this.$route.params.page, displayedPage: -1,
displayItems: this.$store.getters.displayedItems, displayedItems: [],
transition: 'slide-left' transition: 'slide-left'
} }
}, },
computed: { computed: {
activeItems () {
return this.$store.getters.activeItems
},
page () { page () {
return Number(this.$route.params.page) return Number(this.$store.state.route.params.page) || 1
}, },
maxPage () { maxPage () {
const { itemsPerPage, activeItemIds } = this.$store.state const { itemsPerPage, itemIdsByType } = this.$store.state
return Math.floor(activeItemIds.length / itemsPerPage) return Math.floor(itemIdsByType[this.type].length / itemsPerPage)
}, },
hasMore () { hasMore () {
return this.page < this.maxPage return this.page < this.maxPage
} }
}, },
created () {
this.displayedPage = this.page
this.displayedItems = this.activeItems
},
mounted () { mounted () {
fetchInitialData(this.$store) if (this.page > this.maxPage || this.page < 1) {
if (this.page > this.maxPage) { this.$router.replace(`/${this.type}/1`)
this.$router.push('/') } else {
fetchInitialData(this.type).then(() => {
this.displayedItems = this.activeItems
})
} }
}, },
watch: { watch: {
'$route' (to, from) { page (to, from) {
this.loading = true this.loading = true
this.$store.dispatch(`FETCH_DISPLAYED_ITEMS`).then(() => { this.$store.dispatch('FETCH_ACTIVE_ITEMS').then(() => {
const toPage = Number(to.params.page) this.transition = to > from ? 'slide-left' : 'slide-right'
const fromPage = Number(from.params.page) this.displayedPage = to
this.transition = toPage > fromPage ? 'slide-left' : 'slide-right' this.displayedItems = this.activeItems
this.displayPage = toPage
this.displayItems = this.$store.getters.displayedItems
this.loading = false this.loading = false
}) })
} }
......
...@@ -3,14 +3,18 @@ import Router from 'vue-router' ...@@ -3,14 +3,18 @@ import Router from 'vue-router'
Vue.use(Router) Vue.use(Router)
import News from '../views/News.vue' import { createStoriesView } from '../views/CreateStoriesView'
import About from '../views/About.vue' import About from '../views/About.vue'
export default new Router({ export default new Router({
mode: 'history', mode: 'history',
routes: [ routes: [
{ path: '/news/:page(\\d+)', component: News }, { path: '/top/:page(\\d+)?', component: createStoriesView('top') },
{ path: '/new/:page(\\d+)?', component: createStoriesView('new') },
{ path: '/show/:page(\\d+)?', component: createStoriesView('show') },
{ path: '/ask/:page(\\d+)?', component: createStoriesView('ask') },
{ path: '/job/:page(\\d+)?', component: createStoriesView('job') },
{ path: '/about', component: About }, { path: '/about', component: About },
{ path: '*', redirect: '/news/1' } { path: '*', redirect: '/top/1' }
] ]
}) })
...@@ -25,9 +25,12 @@ const api = inBrowser ...@@ -25,9 +25,12 @@ const api = inBrowser
function createServerSideAPI () { function createServerSideAPI () {
const api = new Firebase('https://hacker-news.firebaseio.com/v0') const api = new Firebase('https://hacker-news.firebaseio.com/v0')
// cache the latest top stories' ids // cache the latest story ids
api.child(`topstories`).on('value', snapshot => { api.__ids__ = {}
api.__topIds__ = snapshot.val() ;['top', 'new', 'show', 'ask', 'job'].forEach(type => {
api.child(`${type}stories`).on('value', snapshot => {
api.__ids__[type] = snapshot.val()
})
}) })
// warm the cache every 15 min, since the front page changes quite often // warm the cache every 15 min, since the front page changes quite often
...@@ -48,10 +51,10 @@ function fetch (child) { ...@@ -48,10 +51,10 @@ function fetch (child) {
}) })
} }
export function fetchTopIds () { export function fetchIdsByType (type) {
return api.__topIds__ return api.__ids__ && api.__ids__[type]
? Promise.resolve(api.__topIds__) ? Promise.resolve(api.__ids__[type])
: fetch(`topstories`) : fetch(`${type}stories`)
} }
export function watchTopIds (cb) { export function watchTopIds (cb) {
...@@ -63,7 +66,7 @@ export function watchTopIds (cb) { ...@@ -63,7 +66,7 @@ export function watchTopIds (cb) {
} }
export function fetchItem (id, forceRefresh) { export function fetchItem (id, forceRefresh) {
if (!forceRefresh && cache.has(id)) { if (!forceRefresh && cache.get(id)) {
return Promise.resolve(cache.get(id)) return Promise.resolve(cache.get(id))
} else { } else {
return fetch(`item/${id}`).then(item => { return fetch(`item/${id}`).then(item => {
......
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import { watchTopIds, fetchTopIds, fetchItems } from './api' import { watchTopIds, fetchIdsByType, fetchItems } from './api'
Vue.use(Vuex) Vue.use(Vuex)
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { state: {
activeType: null,
itemsPerPage: 20, itemsPerPage: 20,
activeItemIds: [], // the current items being displayed
items: {} activeItemIds: [/* number */],
// fetched items by id. This also serves as a cache to some extent
items: {/* [id: number]: Item */},
// the id lists for each type of stories
// will be periodically updated in realtime
itemIdsByType: {
top: [],
new: [],
show: [],
ask: [],
job: []
}
}, },
actions: { actions: {
FETCH_IDS: ({ commit }) => { FETCH_ACTIVE_IDS: ({ commit, state }) => {
return fetchTopIds().then(ids => { const type = state.activeType
commit('SET_ACTIVE_IDS', { ids }) return fetchIdsByType(type).then(ids => {
commit('SET_IDS', { type, ids })
}) })
}, },
FETCH_DISPLAYED_ITEMS: ({ commit, state }) => { FETCH_ACTIVE_ITEMS: ({ commit, state, getters }) => {
const ids = getDisplayedIds(state) return fetchItems(getters.activeIds).then(items => {
return fetchItems(ids).then(items => {
commit('SET_ITEMS', { items }) commit('SET_ITEMS', { items })
}) })
} }
}, },
mutations: { mutations: {
SET_ACTIVE_IDS: (state, { ids }) => { SET_ACTIVE_TYPE: (state, { type }) => {
state.activeItemIds = ids state.activeType = type
},
SET_IDS: (state, { type, ids }) => {
state.itemIdsByType[type] = ids
}, },
SET_ITEMS: (state, { items }) => { SET_ITEMS: (state, { items }) => {
items.forEach(item => { items.forEach(item => {
...@@ -37,9 +52,19 @@ const store = new Vuex.Store({ ...@@ -37,9 +52,19 @@ const store = new Vuex.Store({
}, },
getters: { getters: {
displayedItems: state => { activeIds (state) {
const ids = getDisplayedIds(state) const { activeType, itemsPerPage, itemIdsByType } = state
return ids.map(id => state.items[id]).filter(_ => _) const page = Number(state.route.params.page) || 1
if (activeType) {
const start = (page - 1) * itemsPerPage
const end = page * itemsPerPage
return itemIdsByType[activeType].slice(start, end)
} else {
return []
}
},
activeItems (state, getters) {
return getters.activeIds.map(id => state.items[id]).filter(_ => _)
} }
} }
}) })
...@@ -47,17 +72,16 @@ const store = new Vuex.Store({ ...@@ -47,17 +72,16 @@ const store = new Vuex.Store({
// watch for realtime top IDs updates on the client // watch for realtime top IDs updates on the client
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
watchTopIds(ids => { watchTopIds(ids => {
store.commit('SET_ACTIVE_IDS', { ids }) store.commit('SET_IDS', { type: 'top', ids })
store.dispatch('FETCH_DISPLAYED_ITEMS') store.dispatch('FETCH_ACTIVE_ITEMS')
}) })
} }
function getDisplayedIds (state) { export function fetchInitialData (type) {
const page = Number(state.route.params.page) || 1 store.commit('SET_ACTIVE_TYPE', { type })
const { itemsPerPage, activeItemIds } = state return store
const start = (page - 1) * itemsPerPage .dispatch('FETCH_ACTIVE_IDS')
const end = page * itemsPerPage .then(() => store.dispatch('FETCH_ACTIVE_ITEMS'))
return activeItemIds.slice(start, end)
} }
export default store export default store
import NewsList from '../components/NewsList.vue'
import { fetchInitialData } from '../store'
export function createStoriesView (type) {
return {
name: `${type}-stories`,
components: {
NewsList
},
prefetch () {
fetchInitialData(type)
},
render (h) {
return h(NewsList, { props: { type }})
}
}
}
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