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

wip

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