Commit dcfc9507 authored by Huseyn Guliyev's avatar Huseyn Guliyev

Fixed Hackernews demo with latest nuxt

Added Eslimt&Prettier integration
parent 2e57d062
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
"eslint:recommended",
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
"plugin:vue/recommended",
"plugin:prettier/recommended"
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
"semi": [2, "never"],
"no-console": "off",
"vue/max-attributes-per-line": "off",
"prettier/prettier": ["error"]
}
}
\ No newline at end of file
module.exports = {
"semi": false
}
\ No newline at end of file
import { initializeApp, database } from 'firebase/app'
import { initializeApp, database } from "firebase/app"
export async function createAPI({ config, version }) {
await import(/* webpackChunkName: "firebase" */ 'firebase/database')
initializeApp(config)
return database().ref(version)
await import(/* webpackChunkName: "firebase" */ "firebase/database")
initializeApp(config)
return database().ref(version)
}
import Firebase from 'firebase'
import LRU from 'lru-cache'
import Firebase from "firebase"
import LRU from "lru-cache"
export async function createAPI ({ config, version }) {
export async function createAPI({ config, version }) {
let api
// this piece of code may run multiple times in development mode,
// so we attach the instantiated API to `process` to avoid duplications
......@@ -21,8 +21,8 @@ export async function createAPI ({ config, version }) {
// cache the latest story ids
api.cachedIds = {}
;['top', 'new', 'show', 'ask', 'job'].forEach(type => {
api.child(`${type}stories`).on('value', snapshot => {
;["top", "new", "show", "ask", "job"].forEach(type => {
api.child(`${type}stories`).on("value", snapshot => {
api.cachedIds[type] = snapshot.val()
})
})
......
// This is aliased in webpack config based on server/client build
import { createAPI } from 'create-api'
import { createAPI } from "create-api"
const logRequests = !!process.env.DEBUG_API
let api = {}
let _api = createAPI({
version: '/v0',
version: "/v0",
config: {
databaseURL: 'https://hacker-news.firebaseio.com'
databaseURL: "https://hacker-news.firebaseio.com"
}
}).then(_api => {
api = _api
......@@ -18,8 +18,6 @@ let _api = createAPI({
}
})
function warmCache() {
if (!api.cachedIds) return
fetchItems((api.cachedIds.top || []).slice(0, 30))
......@@ -35,14 +33,18 @@ async function fetch(child) {
return cache.get(child)
} else {
return new Promise((resolve, reject) => {
api.child(child).once('value', snapshot => {
const val = snapshot.val()
// mark the timestamp when this item is cached
if (val) val.__lastUpdated = Date.now()
cache && cache.set(child, val)
logRequests && console.log(`fetched ${child}.`)
resolve(val)
}, reject)
api.child(child).once(
"value",
snapshot => {
const val = snapshot.val()
// mark the timestamp when this item is cached
if (val) val.__lastUpdated = Date.now()
cache && cache.set(child, val)
logRequests && console.log(`fetched ${child}.`)
resolve(val)
},
reject
)
})
}
}
......@@ -76,8 +78,8 @@ export async function watchList(type, cb) {
cb(snapshot.val())
}
}
ref.on('value', handler)
ref.on("value", handler)
return () => {
ref.off('value', handler)
ref.off("value", handler)
}
}
......@@ -4,71 +4,94 @@
<router-link :to="'/user/' + comment.by">{{ comment.by }}</router-link>
{{ comment.time | timeAgo }} ago
</div>
<div class="text" v-html="comment.text"></div>
<div class="toggle" :class="{ open }" v-if="comment.kids && comment.kids.length">
<a @click="open = !open">{{
open
? '[-]'
: '[+] ' + pluralize(comment.kids.length) + ' collapsed'
}}</a>
<div class="text" v-html="comment.text" />
<div v-if="comment.kids && comment.kids.length" :class="{ open }" class="toggle">
<a @click="open = !open">{{ open ? '[-]' : '[+] ' + pluralize(comment.kids.length) + ' collapsed' }}
</a>
</div>
<ul class="comment-children" v-show="open">
<comment v-for="id in comment.kids" :key="id" :id="id"></comment>
<ul v-show="open" class="comment-children">
<comment v-for="id in comment.kids" :key="id" :id="id" />
</ul>
</li>
</template>
<script>
export default {
name: 'comment',
props: ['id'],
data () {
name: "Comment",
props: {
id: {
type: String,
required: true
}
},
data() {
return {
open: true
}
},
computed: {
comment () {
comment() {
return this.$store.state.items[this.id]
}
},
methods: {
pluralize: n => n + (n === 1 ? ' reply' : ' replies')
pluralize: n => n + (n === 1 ? " reply" : " replies")
}
}
</script>
<style lang="stylus">
.comment-children
.comment-children
margin-left 1.5em
.comment-children {
.comment-children {
margin-left: 1.5em;
}
}
.comment {
border-top: 1px solid #eee;
position: relative;
.by, .text, .toggle {
font-size: 0.9em;
margin: 1em 0;
}
.by {
color: #828282;
a {
color: #828282;
text-decoration: underline;
}
}
.text {
overflow-wrap: break-word;
.comment
border-top 1px solid #eee
position relative
.by, .text, .toggle
font-size .9em
margin 1em 0
.by
color #828282
a
color #828282
text-decoration underline
.text
overflow-wrap break-word
a:hover
color #ff6600
pre
white-space pre-wrap
.toggle
background-color #fffbf2
padding .3em .5em
border-radius 4px
a
color #828282
cursor pointer
&.open
padding 0
background-color transparent
margin-bottom -0.5em
a:hover {
color: #ff6600;
}
pre {
white-space: pre-wrap;
}
}
.toggle {
background-color: #fffbf2;
padding: 0.3em 0.5em;
border-radius: 4px;
a {
color: #828282;
cursor: pointer;
}
&.open {
padding: 0;
background-color: transparent;
margin-bottom: -0.5em;
}
}
}
</style>
......@@ -13,55 +13,71 @@
<br>
<span class="meta">
<span v-if="item.type !== 'job'" class="by">
by <router-link :to="'/user/' + item.by">{{ item.by }}</router-link>
by
<router-link :to="'/user/' + item.by">{{ item.by }}</router-link>
</span>
<span class="time">
{{ item.time | timeAgo }} ago
</span>
<span v-if="item.type !== 'job'" class="comments-link">
| <router-link :to="'/item/' + item.id">{{ item.descendants }} comments</router-link>
|
<router-link :to="'/item/' + item.id">{{ item.descendants }} comments</router-link>
</span>
</span>
<span class="label" v-if="item.type !== 'story'">{{ item.type }}</span>
<span v-if="item.type !== 'story'" class="label" >{{ item.type }}</span>
</li>
</template>
<script>
import { timeAgo } from '../plugins/filters'
import { timeAgo } from "~/plugins/filters"
export default {
name: 'news-item',
props: ['item'],
name: "NewsItem",
props: {
item: {
type: Object,
required: true
}
},
// http://ssr.vuejs.org/en/caching.html#component-level-caching
serverCacheKey: ({ item: { id, __lastUpdated, time }}) => {
serverCacheKey: ({ item: { id, __lastUpdated, time } }) => {
return `${id}::${__lastUpdated}::${timeAgo(time)}`
}
}
</script>
<style lang="stylus">
.news-item
background-color #fff
padding 20px 30px 20px 80px
border-bottom 1px solid #eee
position relative
line-height 20px
.score
color #C75000
font-size 1.1em
font-weight 700
position absolute
top 50%
left 0
width 80px
text-align center
margin-top -10px
.meta, .host
font-size .85em
color #595959
a
color #595959
text-decoration underline
&:hover
color #C75000
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px;
.score {
color: #C75000;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px;
}
.meta, .host {
font-size: 0.85em;
color: #595959;
a {
color: #595959;
text-decoration: underline;
&:hover {
color: #C75000;
}
}
}
}
</style>
<template>
<div class="news-view view">
<item-list-nav :type="type" :page="page" :maxPage="maxPage"></item-list-nav>
<item-list-nav :type="type" :page="page" :max-page="maxPage" />
<transition :name="transition" mode="out-in">
<div class="news-list" :key="displayedPage" v-if="displayedPage > 0">
<div v-if="displayedPage > 0" :key="displayedPage" class="news-list">
<transition-group tag="ul" name="item">
<item v-for="item in displayedItems" :key="item.id" :item="item">
</item>
<item v-for="item in displayedItems" :key="item.id" :item="item" />
</transition-group>
</div>
</transition>
<item-list-nav :type="type" :page="page" :maxPage="maxPage"></item-list-nav>
<item-list-nav :type="type" :page="page" :max-page="maxPage" />
</div>
</template>
<script>
import { watchList } from '../api'
import Item from './Item.vue'
import ItemListNav from './ItemListNav.vue'
import { watchList } from "../api"
import Item from "./Item.vue"
import ItemListNav from "./ItemListNav.vue"
export default {
name: 'item-list',
name: "ItemList",
components: {
Item,
ItemListNav
},
props: {
type: String
type: {
type: String,
required: true
}
},
data() {
return {
transition: 'slide-right',
transition: "slide-right",
displayedPage: Number(this.$route.params.page) || 1,
displayedItems: this.$store.getters.activeItems
}
......@@ -39,19 +41,26 @@ export default {
return Number(this.$route.params.page) || 1
},
maxPage() {
const {itemsPerPage, lists} = this.$store.state
const { itemsPerPage, lists } = this.$store.state
return Math.ceil(lists[this.type].length / itemsPerPage)
}
},
watch: {
page(to, from) {
console.log("Page changed", to, from)
this.loadItems(to, from)
}
},
async beforeMount() {
if (this.$root._isMounted) {
this.loadItems(this.page)
}
// watch the current list for realtime updates
this.unwatchList = await watchList(this.type, ids => {
this.$store.commit('SET_LIST', {type: this.type, ids})
this.$store.dispatch('ENSURE_ACTIVE_ITEMS').then(() => {
this.$store.commit("SET_LIST", { type: this.type, ids })
this.$store.dispatch("ENSURE_ACTIVE_ITEMS").then(() => {
this.displayedItems = this.$store.getters.activeItems
})
})
......@@ -61,70 +70,75 @@ export default {
this.unwatchList()
},
watch: {
page(to, from) {
console.log('Page changed', to, from)
this.loadItems(to, from)
}
},
methods: {
loadItems(to = this.page, from = -1) {
this.$nuxt.$loading.start()
this.$store.dispatch('FETCH_LIST_DATA', {
type: this.type
}).then(() => {
if (this.page < 0 || this.page > this.maxPage) {
this.$router.replace(`/${this.type}/1`)
return
}
this.transition = from === -1
? null
: to > from ? 'slide-left' : 'slide-right'
this.displayedPage = to
this.displayedItems = this.$store.getters.activeItems
this.$nuxt.$loading.finish()
})
this.$store
.dispatch("FETCH_LIST_DATA", {
type: this.type
})
.then(() => {
if (this.page < 0 || this.page > this.maxPage) {
this.$router.replace(`/${this.type}/1`)
return
}
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>
<style lang="stylus">
.news-list
background-color #fff
border-radius 2px
.news-list {
background-color: #fff;
border-radius: 2px;
}
.news-list
margin 10px 0
width 100%
transition all .3s cubic-bezier(.55, 0, .1, 1)
ul
list-style-type none
padding 0
margin 0
.news-list {
margin: 10px 0;
width: 100%;
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
.slide-left-enter, .slide-right-leave-to
opacity 0
transform translate(30px, 0)
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
}
.slide-left-leave-to, .slide-right-enter
opacity 0
transform translate(-30px, 0)
.slide-left-enter, .slide-right-leave-to {
opacity: 0;
transform: translate(30px, 0);
}
.item-move, .item-enter-active, .item-leave-active
transition all .5s cubic-bezier(.55, 0, .1, 1)
.slide-left-leave-to, .slide-right-enter {
opacity: 0;
transform: translate(-30px, 0);
}
.item-enter
opacity 0
transform translate(30px, 0)
.item-move, .item-enter-active, .item-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
.item-enter {
opacity: 0;
transform: translate(30px, 0);
}
.item-leave-active
position absolute
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
@media (max-width: 600px) {
.news-list {
margin: 10px 0;
}
}
</style>
......@@ -11,9 +11,18 @@
<script>
export default {
props: {
type: String,
page: Number,
maxPage: Number
type: {
type: String,
required: true
},
page: {
type: Number,
required: true
},
maxPage: {
type: Number,
required: true
}
},
computed: {
hasMore() {
......@@ -24,16 +33,22 @@ export default {
</script>
<style lang="stylus">
.news-list-nav, .news-list
background-color #fff
border-radius 2px
.news-list-nav, .news-list {
background-color: #fff;
border-radius: 2px;
}
.news-list-nav
padding 15px 30px
text-align center
box-shadow 0 1px 2px rgba(0, 0, 0, .1)
a
margin 0 1em
.disabled
opacity 0.8
.news-list-nav {
padding: 15px 30px;
text-align: center;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
a {
margin: 0 1em;
}
.disabled {
opacity: 0.8;
}
}
</style>
<template>
<transition>
<svg class="spinner" :class="{ show: show }" v-show="show" width="44px" height="44px" viewBox="0 0 44 44">
<circle class="path" fill="none" stroke-width="4" stroke-linecap="round" cx="22" cy="22" r="20"></circle>
<svg v-show="show" :class="{ show: show }" class="spinner" width="44px" height="44px" viewBox="0 0 44 44">
<circle class="path" fill="none" stroke-width="4" stroke-linecap="round" cx="22" cy="22" r="20"/>
</svg>
</transition>
</template>
<script>
export default {
name: 'spinner',
props: ['show'],
name: "Spinner",
props: {
show: {
type: Boolean,
required: true
}
},
serverCacheKey: props => props.show
}
</script>
<style lang="stylus">
$offset = 126
$duration = 1.4s
.spinner
transition opacity .15s ease
animation rotator $duration linear infinite
animation-play-state paused
&.show
animation-play-state running
&.v-enter, &.v-leave-active
opacity 0
&.v-enter-active, &.v-leave
opacity 1
@keyframes rotator
0%
transform scale(0.5) rotate(0deg)
100%
transform scale(0.5) rotate(270deg)
.spinner .path
stroke #ff6600
stroke-dasharray $offset
stroke-dashoffset 0
transform-origin center
animation dash $duration ease-in-out infinite
@keyframes dash
0%
stroke-dashoffset $offset
50%
stroke-dashoffset ($offset/2)
transform rotate(135deg)
100%
stroke-dashoffset $offset
transform rotate(450deg)
$offset = 126;
$duration = 1.4s;
.spinner {
transition: opacity 0.15s ease;
animation: rotator $duration linear infinite;
animation-play-state: paused;
&.show {
animation-play-state: running;
}
&.v-enter, &.v-leave-active {
opacity: 0;
}
&.v-enter-active, &.v-leave {
opacity: 1;
}
}
@keyframes rotator {
0% {
transform: scale(0.5) rotate(0deg);
}
100% {
transform: scale(0.5) rotate(270deg);
}
}
.spinner .path {
stroke: #ff6600;
stroke-dasharray: $offset;
stroke-dashoffset: 0;
transform-origin: center;
animation: dash $duration ease-in-out infinite;
}
@keyframes dash {
0% {
stroke-dashoffset: $offset;
}
50% {
stroke-dashoffset: ($offset / 2);
transform: rotate(135deg);
}
100% {
stroke-dashoffset: $offset;
transform: rotate(450deg);
}
}
</style>
import ItemList from './ItemList.vue'
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) {
export default function createListView(type) {
return {
name: `${type}-stories-view`,
fetch ({ store }) {
return store.dispatch('FETCH_LIST_DATA', { type })
fetch({ store }) {
return store.dispatch("FETCH_LIST_DATA", { type })
},
head: {
title: camelize(type),
title: camelize(type)
},
render (h) {
return h(ItemList, { props: { type }})
render(h) {
return h(ItemList, { props: { type } })
}
}
}
......@@ -15,7 +15,7 @@
</a>
</nav>
</header>
<nuxt nuxt-child-key="none"></nuxt>
<nuxt nuxt-child-key="none" />
</div>
</template>
......@@ -25,7 +25,7 @@ export default {
return {
link: [
// We use $route.path since we don't use query parameters
{ rel: 'canonical', href: `https://hn.nuxtjs.org${this.$route.path}` }
{ rel: "canonical", href: `https://hn.nuxtjs.org${this.$route.path}` }
]
}
}
......@@ -33,78 +33,109 @@ export default {
</script>
<style lang="stylus">
body
font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size 15px
background-color lighten(#eceef1, 30%)
margin 0
padding 0
color #34495e
overflow-y scroll
a
color #34495e
text-decoration none
.header
background-color #188269
z-index 999
height 55px
.inner
max-width 800px
box-sizing border-box
margin 0px auto
padding 15px 5px
a
color #fff
line-height 24px
transition color .15s ease
display inline-block
vertical-align middle
font-weight 300
letter-spacing .075em
margin-right 1.8em
&:hover
color #fff
&.router-link-active, &.nuxt-link-active
color #fff
font-weight 400
&:nth-child(6)
margin-right 0
.github
color #fff
font-size .9em
margin 0
float right
.logo
width 24px
margin-right 10px
display inline-block
vertical-align middle
.view
max-width 800px
margin 0 auto
position relative
.appear-active
transition opacity .4s ease
.page-enter-active, .page-leave-active
transition all .2s ease
.appear, .page-enter, .page-leave-active
opacity 0
@media (max-width 860px)
.header .inner
padding 15px 30px
@media (max-width 600px)
.header
.inner
padding 15px
a
margin-right 1em
.github
display none
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
background-color: lighten(#eceef1, 30%);
margin: 0;
padding: 0;
color: #34495e;
overflow-y: scroll;
}
a {
color: #34495e;
text-decoration: none;
}
.header {
background-color: #188269;
z-index: 999;
height: 55px;
.inner {
max-width: 800px;
box-sizing: border-box;
margin: 0px auto;
padding: 15px 5px;
}
a {
color: #fff;
line-height: 24px;
transition: color 0.15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: 0.075em;
margin-right: 1.8em;
&:hover {
color: #fff;
}
&.router-link-active, &.nuxt-link-active {
color: #fff;
font-weight: 400;
}
&:nth-child(6) {
margin-right: 0;
}
}
.github {
color: #fff;
font-size: 0.9em;
margin: 0;
float: right;
}
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle;
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative;
}
.appear-active {
transition: opacity 0.4s ease;
}
.page-enter-active, .page-leave-active {
transition: all 0.2s ease;
}
.appear, .page-enter, .page-leave-active {
opacity: 0;
}
@media (max-width: 860px) {
.header .inner {
padding: 15px 30px;
}
}
@media (max-width: 600px) {
.header {
.inner {
padding: 15px;
}
a {
margin-right: 1em;
}
.github {
display: none;
}
}
}
</style>
export default ({ isDev, req, redirect }) => {
// Redirect to https
if (!isDev && req) {
const protocol = req.headers['x-forwarded-proto'] || (req.connection.encrypted ? 'https' : 'http')
if (protocol === 'http') {
const protocol =
req.headers["x-forwarded-proto"] ||
(req.connection.encrypted ? "https" : "http")
if (protocol === "http") {
return redirect(301, `https://${req.headers.host}${req.url}`)
}
}
......
module.exports = {
mode: 'universal',
mode: "universal",
build: {
extend (config, { isClient }) {
config.resolve.alias['create-api'] = `./create-api-${isClient ? 'client' : 'server'}.js`
extend(config, { isClient, isDev }) {
// Run ESLint on save
if (isDev && isClient) {
config.module.rules.push({
enforce: "pre",
test: /\.(js|vue)$/,
loader: "eslint-loader",
exclude: /(node_modules)/
})
}
config.resolve.alias["create-api"] = `./create-api-${
isClient ? "client" : "server"
}.js`
},
vendor: ['firebase']
vendor: ["firebase"]
},
head: {
titleTemplate: 'Nuxt HN | %s',
titleTemplate: "Nuxt HN | %s",
meta: [
{
property: 'og:image',
content: 'https://user-images.githubusercontent.com/904724/26879447-689b56a8-4b91-11e7-968f-5eea1d6c71b4.png'
property: "og:image",
content:
"https://user-images.githubusercontent.com/904724/26879447-689b56a8-4b91-11e7-968f-5eea1d6c71b4.png"
},
{ property: 'twitter:card', content: 'summary_large_image' },
{ property: 'twitter:site', content: '@nuxt_js' },
{ property: "twitter:card", content: "summary_large_image" },
{ property: "twitter:site", content: "@nuxt_js" }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'dns-prefetch', href: 'https://hacker-news.firebaseio.com' }
{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" },
{ rel: "dns-prefetch", href: "https://hacker-news.firebaseio.com" }
]
},
loading: {
color: '#59cc93'
color: "#59cc93"
},
loadingIndicator: {
name: 'rectangle-bounce',
color: 'white',
background: '#188269'
name: "rectangle-bounce",
color: "white",
background: "#188269"
},
manifest: {
name: 'Nuxt Hacker News',
description: 'HackerNews clone built with Nuxt.js',
theme_color: '#188269'
name: "Nuxt Hacker News",
description: "HackerNews clone built with Nuxt.js",
theme_color: "#188269"
},
modules: [
'@nuxtjs/pwa',
'@nuxtjs/component-cache'
],
modules: ["@nuxtjs/pwa", "@nuxtjs/component-cache"],
plugins: [
'~/plugins/vuex-router-sync',
'~/plugins/filters',
'~/plugins/components'
"~/plugins/vuex-router-sync",
"~/plugins/filters",
"~/plugins/components"
],
router: {
middleware: ['https']
middleware: ["https"]
},
render: {
static: {
maxAge: '1y',
setHeaders (res, path) {
if (path.includes('sw.js')) {
res.setHeader('Cache-Control', 'public, max-age=0')
maxAge: "1y",
setHeaders(res, path) {
if (path.includes("sw.js")) {
res.setHeader("Cache-Control", "public, max-age=0")
}
}
}
......
......@@ -21,7 +21,9 @@
"start": "nuxt start",
"dev-spa": "nuxt dev --spa",
"build-spa": "nuxt build --spa",
"start-spa": "nuxt start --spa"
"start-spa": "nuxt start --spa",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore ."
},
"now": {
"alias": "hn.nuxtjs.org"
......@@ -30,14 +32,23 @@
"node": ">=8.0"
},
"dependencies": {
"@nuxtjs/component-cache": "^1.1.0",
"@nuxtjs/pwa": "latest",
"nuxt": "^1.0.0-rc11"
"@nuxtjs/component-cache": "^1.1.1",
"@nuxtjs/pwa": "2.0.6",
"nuxt": "^1.4.0",
"vue-server-renderer": "^2.5.13"
},
"devDependencies": {
"firebase": "^4.3.1",
"@babel/helper-module-imports": "^7.0.0-beta.40",
"babel-eslint": "^8.2.2",
"eslint": "^4.18.2",
"eslint-config-prettier": "^2.9.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-vue": "^4.3.0",
"firebase": "^4.10.1",
"prettier": "^1.11.1",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"vuex-router-sync": "^4.3.2"
"stylus-loader": "^3.0.2",
"vuex-router-sync": "^5.0.0"
}
}
<script>
import createListView from '~/components/createListView'
import createListView from "~/components/createListView"
export default createListView('ask')
export default createListView("ask")
</script>
<template></template>
<script>
export default {
fetch({redirect}) {
redirect('/top')
}
export default {
fetch({ redirect }) {
redirect("/top")
}
}
</script>
<template>
<div class="item-view view" v-if="item">
<div v-if="item" class="item-view view" >
<template v-if="item">
<div class="item-view-header">
<a :href="item.url" target="_blank">
......@@ -9,19 +9,18 @@
({{ item.url | host }})
</span>
<p class="meta">
{{ item.score }} points
| by
{{ item.score }} points | by
<router-link :to="'/user/' + item.by">{{ item.by }}</router-link>
{{ item.time | timeAgo }} ago
</p>
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{{ item.kids ? item.descendants + ' comments' : 'No comments yet.'}}
<spinner :show="loading"></spinner>
{{ item.kids ? item.descendants + ' comments' : 'No comments yet.' }}
<spinner :show="loading"/>
</p>
<ul v-if="!loading" class="comment-children">
<comment v-for="id in item.kids" :key="id" :id="id"></comment>
<comment v-for="id in item.kids" :key="id" :id="id"/>
</ul>
</div>
</template>
......@@ -29,105 +28,126 @@
</template>
<script>
import Spinner from '../../components/Spinner.vue'
import Comment from '../../components/Comment.vue'
export default {
name: 'item-view',
components: {Spinner, Comment},
data: () => ({
loading: true
}),
head() {
return {
title: this.item.title
}
},
computed: {
item() {
return this.$store.state.items[this.$route.params.id]
}
},
// We only fetch the item itself before entering the view, because
// it might take a long time to load threads with hundreds of comments
// due to how the HN Firebase API works.
asyncData({store, route: {params: {id}}}) {
return store.dispatch('FETCH_ITEMS', {ids: [id]})
},
// Fetch comments when mounted on the client
beforeMount() {
this.fetchComments()
},
// refetch comments if item changed
watch: {
item: 'fetchComments'
},
methods: {
fetchComments() {
this.loading = true
fetchComments(this.$store, this.item).then(() => {
this.loading = false
})
}
import Spinner from "~/components/Spinner.vue"
import Comment from "~/components/Comment.vue"
export default {
name: "ItemView",
components: { Spinner, Comment },
data: () => ({
loading: true
}),
head() {
return {
title: this.item.title
}
},
computed: {
item() {
return this.$store.state.items[this.$route.params.id]
}
},
// We only fetch the item itself before entering the view, because
// it might take a long time to load threads with hundreds of comments
// due to how the HN Firebase API works.
asyncData({ store, route: { params: { id } } }) {
return store.dispatch("FETCH_ITEMS", { ids: [id] })
},
// refetch comments if item changed
watch: {
item: "fetchComments"
},
// Fetch comments when mounted on the client
beforeMount() {
this.fetchComments()
},
methods: {
fetchComments() {
this.loading = true
fetchComments(this.$store, this.item).then(() => {
this.loading = false
})
}
}
}
// recursively fetch all descendent comments
function fetchComments(store, item) {
if (item && item.kids) {
return store.dispatch('FETCH_ITEMS', {
// recursively fetch all descendent comments
function fetchComments(store, item) {
if (item && item.kids) {
return store
.dispatch("FETCH_ITEMS", {
ids: item.kids
}).then(() => Promise.all(item.kids.map(id => {
return fetchComments(store, store.state.items[id])
})))
}
return Promise.resolve()
})
.then(() =>
Promise.all(
item.kids.map(id => {
return fetchComments(store, store.state.items[id])
})
)
)
}
return Promise.resolve()
}
</script>
<style lang="stylus">
.item-view-header
background-color #fff
padding 1.8em 2em 1em
box-shadow 0 1px 2px rgba(0, 0, 0, .1)
h1
display inline
font-size 1.5em
margin 0
margin-right .5em
.host, .meta, .meta a
color #595959
.meta a
text-decoration underline
.item-view-comments
background-color #fff
margin-top 10px
padding 0 2em .5em
.item-view-comments-header
margin 0
font-size 1.1em
padding 1em 0
position relative
.spinner
display inline-block
margin -15px 0
.comment-children
list-style-type none
padding 0
margin 0
@media (max-width 600px)
.item-view-header
h1
font-size 1.25em
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: 0.5em;
}
.host, .meta, .meta a {
color: #595959;
}
.meta a {
text-decoration: underline;
}
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em 0.5em;
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative;
.spinner {
display: inline-block;
margin: -15px 0;
}
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0;
}
@media (max-width: 600px) {
.item-view-header {
h1 {
font-size: 1.25em;
}
}
}
</style>
<script>
import createListView from '~/components/createListView'
import createListView from "~/components/createListView"
export default createListView('job')
export default createListView("job")
</script>
<script>
import createListView from '~/components/createListView'
import createListView from "~/components/createListView"
export default createListView('new')
export default createListView("new")
</script>
<script>
import createListView from '~/components/createListView'
import createListView from "~/components/createListView"
export default createListView('show')
export default createListView("show")
</script>
<script>
import createListView from '~/components/createListView'
import createListView from "~/components/createListView"
export default createListView('top')
export default createListView("top")
</script>
......@@ -3,9 +3,11 @@
<template v-if="user">
<h1>User : {{ user.id }}</h1>
<ul class="meta">
<li><span class="label">Created:</span> {{ user.created | timeAgo }} ago</li>
<li><span class="label">Karma:</span> {{user.karma}}</li>
<li v-if="user.about" v-html="user.about" class="about"></li>
<li>
<span class="label">Created:</span> {{ user.created | timeAgo }} ago</li>
<li>
<span class="label">Karma:</span> {{ user.karma }}</li>
<li v-if="user.about" class="about" v-html="user.about" />
</ul>
<p class="links">
<a :href="'https://news.ycombinator.com/submitted?id=' + user.id">submissions</a> |
......@@ -19,44 +21,54 @@
</template>
<script>
export default {
name: "UserView",
export default {
name: 'user-view',
computed: {
user() {
return this.$store.state.users[this.$route.params.id]
}
},
computed: {
user() {
return this.$store.state.users[this.$route.params.id]
}
},
head() {
return {
title: this.user.id || "User not found"
}
},
head() {
return {
title: this.user.id || 'User not found'
}
},
fetch({store, route: {params: {id}}}) {
return store.dispatch('FETCH_USER', {id})
},
fetch({ store, route: { params: { id } } }) {
return store.dispatch("FETCH_USER", { id })
}
}
</script>
<style lang="stylus">
.user-view
background-color #fff
box-sizing border-box
padding 2em 3em
h1
margin 0
font-size 1.5em
.meta
list-style-type none
padding 0
.label
display inline-block
min-width 4em
.about
margin 1em 0
.links a
text-decoration underline
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em;
h1 {
margin: 0;
font-size: 1.5em;
}
.meta {
list-style-type: none;
padding: 0;
}
.label {
display: inline-block;
min-width: 4em;
}
.about {
margin: 1em 0;
}
.links a {
text-decoration: underline;
}
}
</style>
// Import common components to optimize chunks size
import '~/components/createListView'
import "~/components/createListView"
import Vue from 'vue'
import Vue from "vue"
export function host(url) {
const host = url.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
const parts = host.split('.').slice(-3)
if (parts[0] === 'www') parts.shift()
return parts.join('.')
const host = url.replace(/^https?:\/\//, "").replace(/\/.*$/, "")
const parts = host.split(".").slice(-3)
if (parts[0] === "www") parts.shift()
return parts.join(".")
}
export function timeAgo(time) {
const between = Date.now() / 1000 - Number(time)
if (between < 3600) {
return pluralize(~~(between / 60), ' minute')
return pluralize(~~(between / 60), " minute")
} else if (between < 86400) {
return pluralize(~~(between / 3600), ' hour')
return pluralize(~~(between / 3600), " hour")
} else {
return pluralize(~~(between / 86400), ' day')
return pluralize(~~(between / 86400), " day")
}
}
......@@ -22,7 +22,7 @@ function pluralize(time, label) {
if (time === 1) {
return time + label
}
return time + label + 's'
return time + label + "s"
}
const filters = {
......
import Vue from 'vue'
import { sync } from 'vuex-router-sync'
import { sync } from "vuex-router-sync"
export default function ({ app, store }) {
export default function({ app, store }) {
sync(store, app.router)
}
import {
fetchUser,
fetchItems,
fetchIdsByType
} from '../api'
import { fetchUser, fetchItems, fetchIdsByType } from "../api"
export default {
// ensure data for rendering given list type
FETCH_LIST_DATA: ({ commit, dispatch, state }, { type }) => {
commit('SET_ACTIVE_TYPE', { type })
FETCH_LIST_DATA: ({ commit, dispatch }, { type }) => {
commit("SET_ACTIVE_TYPE", { type })
return fetchIdsByType(type)
.then(ids => commit('SET_LIST', { type, ids }))
.then(() => dispatch('ENSURE_ACTIVE_ITEMS'))
.then(ids => commit("SET_LIST", { type, ids }))
.then(() => dispatch("ENSURE_ACTIVE_ITEMS"))
},
// ensure all active items are fetched
ENSURE_ACTIVE_ITEMS: ({ dispatch, getters }) => {
return dispatch('FETCH_ITEMS', {
return dispatch("FETCH_ITEMS", {
ids: getters.activeIds
})
},
......@@ -35,7 +31,7 @@ export default {
return false
})
if (ids.length) {
return fetchItems(ids).then(items => commit('SET_ITEMS', { items }))
return fetchItems(ids).then(items => commit("SET_ITEMS", { items }))
} else {
return Promise.resolve()
}
......@@ -44,6 +40,6 @@ export default {
FETCH_USER: ({ commit, state }, { id }) => {
return state.users[id]
? Promise.resolve(state.users[id])
: fetchUser(id).then(user => commit('SET_USER', { id, user }))
: fetchUser(id).then(user => commit("SET_USER", { id, user }))
}
}
export default {
// ids of the items that should be currently displayed based on
// current list type and current pagination
activeIds (state) {
activeIds(state) {
const { activeType, itemsPerPage, lists } = state
if (!activeType) {
......@@ -17,7 +17,7 @@ export default {
// items that should be currently displayed.
// this Array may not be fully fetched.
activeItems (state, getters) {
activeItems(state, getters) {
return getters.activeIds.map(id => state.items[id]).filter(_ => _)
}
}
import Vuex from 'vuex'
import Vuex from "vuex"
import actions from './actions'
import mutations from './mutations'
import getters from './getters'
import actions from "./actions"
import mutations from "./mutations"
import getters from "./getters"
export default () => {
return new Vuex.Store({
state: {
activeType: null,
itemsPerPage: 20,
items: {/* [id: number]: Item */},
users: {/* [id: string]: User */},
items: {
/* [id: number]: Item */
},
users: {
/* [id: string]: User */
},
lists: {
top: [/* number */],
top: [
/* number */
],
new: [],
show: [],
ask: [],
......
import Vue from 'vue'
import Vue from "vue"
export default {
SET_ACTIVE_TYPE: (state, { type }) => {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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