Skip to content

Commit 144379b

Browse files
committed
wip
1 parent 61d8c28 commit 144379b

File tree

9 files changed

+214
-146
lines changed

9 files changed

+214
-146
lines changed

src/App.vue

+11-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
<a href="http://vuejs.org" target="_blank">
55
<img class="logo" src="./assets/logo.png">
66
</a>
7-
<router-link to="/">News</router-link>
7+
<router-link to="/top">Top</router-link>
8+
<router-link to="/new">New</router-link>
9+
<router-link to="/show">Show</router-link>
10+
<router-link to="/ask">Ask</router-link>
11+
<router-link to="/job">Jobs</router-link>
812
<router-link to="/about">About</router-link>
913
</div>
1014
<transition name="view" mode="out-in">
@@ -27,8 +31,10 @@ body
2731
opacity 0
2832
2933
a
30-
color #4fc08d
31-
32-
a.disabled
33-
color #999
34+
color #333
35+
transition color .15s ease
36+
&.router-link-active
37+
color #4fc08d
38+
&.disabled
39+
color #999
3440
</style>

src/components/NewsList.vue

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<template>
2+
<div>
3+
<ul class="news-list-nav">
4+
<li>
5+
<router-link v-if="page > 1" :to="'/' + type + '/' + (page - 1)">prev</router-link>
6+
<a v-else class="disabled">prev</a>
7+
</li>
8+
<li>
9+
<router-link v-if="hasMore" :to="'/' + type + '/' + (page + 1)">more...</router-link>
10+
<a v-else class="disabled">more...</a>
11+
</li>
12+
<li>
13+
<spinner :show="loading"></spinner>
14+
</li>
15+
</ul>
16+
<transition :name="transition">
17+
<div class="news-list" :key="displayedPage">
18+
<transition-group tag="ul" name="item">
19+
<news-item v-for="item in displayedItems" :key="item.id" :item="item">
20+
</news-item>
21+
</transition-group>
22+
</div>
23+
</transition>
24+
</div>
25+
</template>
26+
27+
<script>
28+
import Spinner from './Spinner.vue'
29+
import NewsItem from './NewsItem.vue'
30+
import { fetchInitialData } from '../store'
31+
32+
export default {
33+
name: 'NewsList',
34+
35+
components: {
36+
Spinner,
37+
NewsItem
38+
},
39+
40+
props: {
41+
type: String
42+
},
43+
44+
data () {
45+
return {
46+
loading: false,
47+
displayedPage: -1,
48+
displayedItems: [],
49+
transition: 'slide-left'
50+
}
51+
},
52+
53+
computed: {
54+
activeItems () {
55+
return this.$store.getters.activeItems
56+
},
57+
page () {
58+
return Number(this.$store.state.route.params.page) || 1
59+
},
60+
maxPage () {
61+
const { itemsPerPage, itemIdsByType } = this.$store.state
62+
return Math.floor(itemIdsByType[this.type].length / itemsPerPage)
63+
},
64+
hasMore () {
65+
return this.page < this.maxPage
66+
}
67+
},
68+
69+
created () {
70+
this.displayedPage = this.page
71+
this.displayedItems = this.activeItems
72+
},
73+
74+
mounted () {
75+
if (this.page > this.maxPage || this.page < 1) {
76+
this.$router.replace(`/${this.type}/1`)
77+
} else {
78+
fetchInitialData(this.type).then(() => {
79+
this.displayedItems = this.activeItems
80+
})
81+
}
82+
},
83+
84+
watch: {
85+
page (to, from) {
86+
this.loading = true
87+
this.$store.dispatch('FETCH_ACTIVE_ITEMS').then(() => {
88+
this.transition = to > from ? 'slide-left' : 'slide-right'
89+
this.displayedPage = to
90+
this.displayedItems = this.activeItems
91+
this.loading = false
92+
})
93+
}
94+
}
95+
}
96+
</script>
97+
98+
<style lang="stylus">
99+
.news-list
100+
position absolute
101+
transition all .5s cubic-bezier(.55,0,.1,1)
102+
103+
.slide-left-enter, .slide-right-leave-active
104+
opacity 0
105+
transform translate(30px, 0)
106+
107+
.slide-left-leave-active, .slide-right-enter
108+
opacity 0
109+
transform translate(-30px, 0)
110+
111+
.item-move, .item-enter-active, .item-leave-active
112+
transition all .5s cubic-bezier(.55,0,.1,1)
113+
114+
.item-enter
115+
opacity 0
116+
transform translate(30px, 0)
117+
118+
.item-leave-active
119+
position absolute
120+
opacity 0
121+
transform translate(30px, 0)
122+
</style>

src/router/index.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import Router from 'vue-router'
33

44
Vue.use(Router)
55

6-
import News from '../views/News.vue'
6+
import { createStoriesView } from '../views/CreateStoriesView'
77
import About from '../views/About.vue'
88

99
export default new Router({
1010
mode: 'history',
1111
routes: [
12-
{ path: '/news/:page(\\d+)', component: News },
12+
{ path: '/top/:page(\\d+)?', component: createStoriesView('top') },
13+
{ path: '/new/:page(\\d+)?', component: createStoriesView('new') },
14+
{ path: '/show/:page(\\d+)?', component: createStoriesView('show') },
15+
{ path: '/ask/:page(\\d+)?', component: createStoriesView('ask') },
16+
{ path: '/job/:page(\\d+)?', component: createStoriesView('job') },
1317
{ path: '/about', component: About },
14-
{ path: '*', redirect: '/news/1' }
18+
{ path: '*', redirect: '/top/1' }
1519
]
1620
})

src/store/api.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ const api = inBrowser
2525
function createServerSideAPI () {
2626
const api = new Firebase('https://hacker-news.firebaseio.com/v0')
2727

28-
// cache the latest top stories' ids
29-
api.child(`topstories`).on('value', snapshot => {
30-
api.__topIds__ = snapshot.val()
28+
// cache the latest story ids
29+
api.__ids__ = {}
30+
;['top', 'new', 'show', 'ask', 'job'].forEach(type => {
31+
api.child(`${type}stories`).on('value', snapshot => {
32+
api.__ids__[type] = snapshot.val()
33+
})
3134
})
3235

3336
// warm the cache every 15 min, since the front page changes quite often
@@ -48,10 +51,10 @@ function fetch (child) {
4851
})
4952
}
5053

51-
export function fetchTopIds () {
52-
return api.__topIds__
53-
? Promise.resolve(api.__topIds__)
54-
: fetch(`topstories`)
54+
export function fetchIdsByType (type) {
55+
return api.__ids__ && api.__ids__[type]
56+
? Promise.resolve(api.__ids__[type])
57+
: fetch(`${type}stories`)
5558
}
5659

5760
export function watchTopIds (cb) {
@@ -63,7 +66,7 @@ export function watchTopIds (cb) {
6366
}
6467

6568
export function fetchItem (id, forceRefresh) {
66-
if (!forceRefresh && cache.has(id)) {
69+
if (!forceRefresh && cache.get(id)) {
6770
return Promise.resolve(cache.get(id))
6871
} else {
6972
return fetch(`item/${id}`).then(item => {

src/store/index.js

+46-22
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,48 @@
11
import Vue from 'vue'
22
import Vuex from 'vuex'
3-
import { watchTopIds, fetchTopIds, fetchItems } from './api'
3+
import { watchTopIds, fetchIdsByType, fetchItems } from './api'
44

55
Vue.use(Vuex)
66

77
const store = new Vuex.Store({
88
state: {
9+
activeType: null,
910
itemsPerPage: 20,
10-
activeItemIds: [],
11-
items: {}
11+
// the current items being displayed
12+
activeItemIds: [/* number */],
13+
// fetched items by id. This also serves as a cache to some extent
14+
items: {/* [id: number]: Item */},
15+
// the id lists for each type of stories
16+
// will be periodically updated in realtime
17+
itemIdsByType: {
18+
top: [],
19+
new: [],
20+
show: [],
21+
ask: [],
22+
job: []
23+
}
1224
},
1325

1426
actions: {
15-
FETCH_IDS: ({ commit }) => {
16-
return fetchTopIds().then(ids => {
17-
commit('SET_ACTIVE_IDS', { ids })
27+
FETCH_ACTIVE_IDS: ({ commit, state }) => {
28+
const type = state.activeType
29+
return fetchIdsByType(type).then(ids => {
30+
commit('SET_IDS', { type, ids })
1831
})
1932
},
20-
FETCH_DISPLAYED_ITEMS: ({ commit, state }) => {
21-
const ids = getDisplayedIds(state)
22-
return fetchItems(ids).then(items => {
33+
FETCH_ACTIVE_ITEMS: ({ commit, state, getters }) => {
34+
return fetchItems(getters.activeIds).then(items => {
2335
commit('SET_ITEMS', { items })
2436
})
2537
}
2638
},
2739

2840
mutations: {
29-
SET_ACTIVE_IDS: (state, { ids }) => {
30-
state.activeItemIds = ids
41+
SET_ACTIVE_TYPE: (state, { type }) => {
42+
state.activeType = type
43+
},
44+
SET_IDS: (state, { type, ids }) => {
45+
state.itemIdsByType[type] = ids
3146
},
3247
SET_ITEMS: (state, { items }) => {
3348
items.forEach(item => {
@@ -37,27 +52,36 @@ const store = new Vuex.Store({
3752
},
3853

3954
getters: {
40-
displayedItems: state => {
41-
const ids = getDisplayedIds(state)
42-
return ids.map(id => state.items[id]).filter(_ => _)
55+
activeIds (state) {
56+
const { activeType, itemsPerPage, itemIdsByType } = state
57+
const page = Number(state.route.params.page) || 1
58+
if (activeType) {
59+
const start = (page - 1) * itemsPerPage
60+
const end = page * itemsPerPage
61+
return itemIdsByType[activeType].slice(start, end)
62+
} else {
63+
return []
64+
}
65+
},
66+
activeItems (state, getters) {
67+
return getters.activeIds.map(id => state.items[id]).filter(_ => _)
4368
}
4469
}
4570
})
4671

4772
// watch for realtime top IDs updates on the client
4873
if (typeof window !== 'undefined') {
4974
watchTopIds(ids => {
50-
store.commit('SET_ACTIVE_IDS', { ids })
51-
store.dispatch('FETCH_DISPLAYED_ITEMS')
75+
store.commit('SET_IDS', { type: 'top', ids })
76+
store.dispatch('FETCH_ACTIVE_ITEMS')
5277
})
5378
}
5479

55-
function getDisplayedIds (state) {
56-
const page = Number(state.route.params.page) || 1
57-
const { itemsPerPage, activeItemIds } = state
58-
const start = (page - 1) * itemsPerPage
59-
const end = page * itemsPerPage
60-
return activeItemIds.slice(start, end)
80+
export function fetchInitialData (type) {
81+
store.commit('SET_ACTIVE_TYPE', { type })
82+
return store
83+
.dispatch('FETCH_ACTIVE_IDS')
84+
.then(() => store.dispatch('FETCH_ACTIVE_ITEMS'))
6185
}
6286

6387
export default store

src/views/CreateStoriesView.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import NewsList from '../components/NewsList.vue'
2+
import { fetchInitialData } from '../store'
3+
4+
export function createStoriesView (type) {
5+
return {
6+
name: `${type}-stories`,
7+
components: {
8+
NewsList
9+
},
10+
prefetch () {
11+
fetchInitialData(type)
12+
},
13+
render (h) {
14+
return h(NewsList, { props: { type }})
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)