[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\"env\", { \"modules\": false }]\n  ],\n  \"plugins\": [\n    \"syntax-dynamic-import\"\n  ]\n}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules/\ndist/\nnpm-debug.log\nyarn-error.log\n.idea\n*.iml"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013-present, Yuxi (Evan) You\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# vue-hackernews-2.0\n\nHackerNews clone built with Vue 2.0 + vue-router + vuex, with server-side rendering.\n\n<p align=\"center\">\n  <a href=\"https://vue-hn.herokuapp.com\" target=\"_blank\">\n    <img src=\"https://cloud.githubusercontent.com/assets/499550/17546273/5aabc5fc-5eaf-11e6-8d6a-ad00937e8bd6.png\" width=\"700px\">\n    <br>\n    Live Demo\n  </a>\n</p>\n\n## Features\n\n> Note: in practice, it is unnecessary to code-split for an app of this size (where each async chunk is only a few kilobytes), nor is it optimal to extract an extra CSS file (which is only 1kb) -- they are used simply because this is a demo app showcasing all the supported features.\n\n- Server Side Rendering\n  - Vue + vue-router + vuex working together\n  - Server-side data pre-fetching\n  - Client-side state & DOM hydration\n  - Automatically inlines CSS used by rendered components only\n  - Preload / prefetch resource hints\n  - Route-level code splitting\n- Progressive Web App\n  - App manifest\n  - Service worker\n  - 100/100 Lighthouse score\n- Single-file Vue Components\n  - Hot-reload in development\n  - CSS extraction for production\n- Animation\n  - Effects when switching route views\n  - Real-time list updates with FLIP Animation\n\n## A Note on Performance\n\nThis is a demo primarily aimed at explaining how to build a server-side rendered Vue app, as a companion to our SSR documentation. There are a few things we probably won't do in production if we were optimizing for performance, for example:\n\n- This demo uses the Firebase-based HN API to showcase real-time updates, but the Firebase API also comes with a larger bundle, more JavaScript to parse on the client, and doesn't offer an efficient way to batch-fetch pages of items, so it impacts performance quite a bit on a cold start or cache miss.\n\n- In practice, it is unnecessary to code-split for an app of this size (where each async chunk is only a few kilobytes so the extra request isn't really worth it), nor is it optimal to extract an extra CSS file (which is only 1kb).\n\nIt is therefore not recommended to use this app as a reference for Vue SSR performance - instead, do your own benchmarking, and make sure to measure and optimize based on your actual app constraints.\n\n## Architecture Overview\n\n<img width=\"973\" alt=\"screen shot 2016-08-11 at 6 06 57 pm\" src=\"https://cloud.githubusercontent.com/assets/499550/17607895/786a415a-5fee-11e6-9c11-45a2cfdf085c.png\">\n\n**A detailed Vue SSR guide can be found [here](https://ssr.vuejs.org).**\n\n## Build Setup\n\n**Requires Node.js 7+**\n\n``` bash\n# install dependencies\nnpm install # or yarn\n\n# serve in dev mode, with hot reload at localhost:8080\nnpm run dev\n\n# build for production\nnpm run build\n\n# serve in production mode\nnpm start\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "build/setup-dev-server.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst MFS = require('memory-fs')\nconst webpack = require('webpack')\nconst chokidar = require('chokidar')\nconst clientConfig = require('./webpack.client.config')\nconst serverConfig = require('./webpack.server.config')\n\nconst readFile = (fs, file) => {\n  try {\n    return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')\n  } catch (e) {}\n}\n\nmodule.exports = function setupDevServer (app, templatePath, cb) {\n  let bundle\n  let template\n  let clientManifest\n\n  let ready\n  const readyPromise = new Promise(r => { ready = r })\n  const update = () => {\n    if (bundle && clientManifest) {\n      ready()\n      cb(bundle, {\n        template,\n        clientManifest\n      })\n    }\n  }\n\n  // read template from disk and watch\n  template = fs.readFileSync(templatePath, 'utf-8')\n  chokidar.watch(templatePath).on('change', () => {\n    template = fs.readFileSync(templatePath, 'utf-8')\n    console.log('index.html template updated.')\n    update()\n  })\n\n  // modify client config to work with hot middleware\n  clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]\n  clientConfig.output.filename = '[name].js'\n  clientConfig.plugins.push(\n    new webpack.HotModuleReplacementPlugin(),\n    new webpack.NoEmitOnErrorsPlugin()\n  )\n\n  // dev middleware\n  const clientCompiler = webpack(clientConfig)\n  const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {\n    publicPath: clientConfig.output.publicPath,\n    noInfo: true\n  })\n  app.use(devMiddleware)\n  clientCompiler.plugin('done', stats => {\n    stats = stats.toJson()\n    stats.errors.forEach(err => console.error(err))\n    stats.warnings.forEach(err => console.warn(err))\n    if (stats.errors.length) return\n    clientManifest = JSON.parse(readFile(\n      devMiddleware.fileSystem,\n      'vue-ssr-client-manifest.json'\n    ))\n    update()\n  })\n\n  // hot middleware\n  app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))\n\n  // watch and update server renderer\n  const serverCompiler = webpack(serverConfig)\n  const mfs = new MFS()\n  serverCompiler.outputFileSystem = mfs\n  serverCompiler.watch({}, (err, stats) => {\n    if (err) throw err\n    stats = stats.toJson()\n    if (stats.errors.length) return\n\n    // read bundle generated by vue-ssr-webpack-plugin\n    bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))\n    update()\n  })\n\n  return readyPromise\n}\n"
  },
  {
    "path": "build/webpack.base.config.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst ExtractTextPlugin = require('extract-text-webpack-plugin')\nconst FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')\nconst { VueLoaderPlugin } = require('vue-loader')\n\nconst isProd = process.env.NODE_ENV === 'production'\n\nmodule.exports = {\n  devtool: isProd\n    ? false\n    : '#cheap-module-source-map',\n  output: {\n    path: path.resolve(__dirname, '../dist'),\n    publicPath: '/dist/',\n    filename: '[name].[chunkhash].js'\n  },\n  resolve: {\n    alias: {\n      'public': path.resolve(__dirname, '../public')\n    }\n  },\n  module: {\n    noParse: /es6-promise\\.js$/, // avoid webpack shimming process\n    rules: [\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: {\n          compilerOptions: {\n            preserveWhitespace: false\n          }\n        }\n      },\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        exclude: /node_modules/\n      },\n      {\n        test: /\\.(png|jpg|gif|svg)$/,\n        loader: 'url-loader',\n        options: {\n          limit: 10000,\n          name: '[name].[ext]?[hash]'\n        }\n      },\n      {\n        test: /\\.styl(us)?$/,\n        use: isProd\n          ? ExtractTextPlugin.extract({\n              use: [\n                {\n                  loader: 'css-loader',\n                  options: { minimize: true }\n                },\n                'stylus-loader'\n              ],\n              fallback: 'vue-style-loader'\n            })\n          : ['vue-style-loader', 'css-loader', 'stylus-loader']\n      },\n    ]\n  },\n  performance: {\n    hints: false\n  },\n  plugins: isProd\n    ? [\n        new VueLoaderPlugin(),\n        new webpack.optimize.UglifyJsPlugin({\n          compress: { warnings: false }\n        }),\n        new webpack.optimize.ModuleConcatenationPlugin(),\n        new ExtractTextPlugin({\n          filename: 'common.[chunkhash].css'\n        })\n      ]\n    : [\n        new VueLoaderPlugin(),\n        new FriendlyErrorsPlugin()\n      ]\n}\n"
  },
  {
    "path": "build/webpack.client.config.js",
    "content": "const webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst base = require('./webpack.base.config')\nconst SWPrecachePlugin = require('sw-precache-webpack-plugin')\nconst VueSSRClientPlugin = require('vue-server-renderer/client-plugin')\n\nconst config = merge(base, {\n  entry: {\n    app: './src/entry-client.js'\n  },\n  resolve: {\n    alias: {\n      'create-api': './create-api-client.js'\n    }\n  },\n  plugins: [\n    // strip dev-only code in Vue source\n    new webpack.DefinePlugin({\n      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),\n      'process.env.VUE_ENV': '\"client\"'\n    }),\n    // extract vendor chunks for better caching\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'vendor',\n      minChunks: function (module) {\n        // a module is extracted into the vendor chunk if...\n        return (\n          // it's inside node_modules\n          /node_modules/.test(module.context) &&\n          // and not a CSS file (due to extract-text-webpack-plugin limitation)\n          !/\\.css$/.test(module.request)\n        )\n      }\n    }),\n    // extract webpack runtime & manifest to avoid vendor chunk hash changing\n    // on every build.\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'manifest'\n    }),\n    new VueSSRClientPlugin()\n  ]\n})\n\nif (process.env.NODE_ENV === 'production') {\n  config.plugins.push(\n    // auto generate service worker\n    new SWPrecachePlugin({\n      cacheId: 'vue-hn',\n      filename: 'service-worker.js',\n      minify: true,\n      dontCacheBustUrlsMatching: /./,\n      staticFileGlobsIgnorePatterns: [/\\.map$/, /\\.json$/],\n      runtimeCaching: [\n        {\n          urlPattern: '/',\n          handler: 'networkFirst'\n        },\n        {\n          urlPattern: /\\/(top|new|show|ask|jobs)/,\n          handler: 'networkFirst'\n        },\n        {\n          urlPattern: '/item/:id',\n          handler: 'networkFirst'\n        },\n        {\n          urlPattern: '/user/:id',\n          handler: 'networkFirst'\n        }\n      ]\n    })\n  )\n}\n\nmodule.exports = config\n"
  },
  {
    "path": "build/webpack.server.config.js",
    "content": "const webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst base = require('./webpack.base.config')\nconst nodeExternals = require('webpack-node-externals')\nconst VueSSRServerPlugin = require('vue-server-renderer/server-plugin')\n\nmodule.exports = merge(base, {\n  target: 'node',\n  devtool: '#source-map',\n  entry: './src/entry-server.js',\n  output: {\n    filename: 'server-bundle.js',\n    libraryTarget: 'commonjs2'\n  },\n  resolve: {\n    alias: {\n      'create-api': './create-api-server.js'\n    }\n  },\n  // https://webpack.js.org/configuration/externals/#externals\n  // https://github.com/liady/webpack-node-externals\n  externals: nodeExternals({\n    // do not externalize CSS files in case we need to import it from a dep\n    whitelist: /\\.css$/\n  }),\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),\n      'process.env.VUE_ENV': '\"server\"'\n    }),\n    new VueSSRServerPlugin()\n  ]\n})\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"name\": \"Vue Hackernews 2.0\",\n  \"short_name\": \"Vue HN\",\n  \"icons\": [{\n      \"src\": \"/public/logo-120.png\",\n      \"sizes\": \"120x120\",\n      \"type\": \"image/png\"\n    }, {\n      \"src\": \"/public/logo-144.png\",\n      \"sizes\": \"144x144\",\n      \"type\": \"image/png\"\n    }, {\n      \"src\": \"/public/logo-152.png\",\n      \"sizes\": \"152x152\",\n      \"type\": \"image/png\"\n    }, {\n      \"src\": \"/public/logo-192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    }, {\n      \"src\": \"/public/logo-256.png\",\n      \"sizes\": \"256x256\",\n      \"type\": \"image/png\"\n    }, {\n      \"src\": \"/public/logo-384.png\",\n      \"sizes\": \"384x384\",\n      \"type\": \"image/png\"\n    }, {\n    \"src\": \"/public/logo-512.png\",\n    \"sizes\": \"512x512\",\n    \"type\": \"image/png\"\n    }],\n  \"start_url\": \"/\",\n  \"background_color\": \"#f2f3f5\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#f60\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"vue-hackernews-2.0\",\n  \"description\": \"A Vue.js project\",\n  \"author\": \"Evan You <yyx990803@gmail.com>\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"node server\",\n    \"start\": \"cross-env NODE_ENV=production node server\",\n    \"build\": \"rimraf dist && npm run build:client && npm run build:server\",\n    \"build:client\": \"cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules\",\n    \"build:server\": \"cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules\",\n    \"postinstall\": \"npm run build\"\n  },\n  \"engines\": {\n    \"node\": \">=7.0\",\n    \"npm\": \">=4.0\"\n  },\n  \"dependencies\": {\n    \"compression\": \"^1.7.1\",\n    \"cross-env\": \"^5.1.1\",\n    \"es6-promise\": \"^4.1.1\",\n    \"express\": \"^4.16.2\",\n    \"extract-text-webpack-plugin\": \"^3.0.2\",\n    \"firebase\": \"4.6.2\",\n    \"lru-cache\": \"^4.1.1\",\n    \"route-cache\": \"0.4.3\",\n    \"serve-favicon\": \"^2.4.5\",\n    \"vue\": \"^2.5.22\",\n    \"vue-router\": \"^3.0.1\",\n    \"vue-server-renderer\": \"^2.5.22\",\n    \"vuex\": \"^3.0.1\",\n    \"vuex-router-sync\": \"^5.0.0\"\n  },\n  \"devDependencies\": {\n    \"autoprefixer\": \"^7.1.6\",\n    \"babel-core\": \"^6.26.0\",\n    \"babel-loader\": \"^7.1.2\",\n    \"babel-plugin-syntax-dynamic-import\": \"^6.18.0\",\n    \"babel-preset-env\": \"^1.6.1\",\n    \"chokidar\": \"^1.7.0\",\n    \"css-loader\": \"^0.28.7\",\n    \"file-loader\": \"^1.1.5\",\n    \"friendly-errors-webpack-plugin\": \"^1.6.1\",\n    \"rimraf\": \"^2.6.2\",\n    \"stylus\": \"^0.54.5\",\n    \"stylus-loader\": \"^3.0.1\",\n    \"sw-precache-webpack-plugin\": \"^0.11.4\",\n    \"url-loader\": \"^0.6.2\",\n    \"vue-loader\": \"^15.3.0\",\n    \"vue-template-compiler\": \"^2.5.22\",\n    \"webpack\": \"^3.8.1\",\n    \"webpack-dev-middleware\": \"^1.12.0\",\n    \"webpack-hot-middleware\": \"^2.20.0\",\n    \"webpack-merge\": \"^4.2.1\",\n    \"webpack-node-externals\": \"^1.7.2\"\n  }\n}\n"
  },
  {
    "path": "server.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst LRU = require('lru-cache')\nconst express = require('express')\nconst favicon = require('serve-favicon')\nconst compression = require('compression')\nconst microcache = require('route-cache')\nconst resolve = file => path.resolve(__dirname, file)\nconst { createBundleRenderer } = require('vue-server-renderer')\n\nconst isProd = process.env.NODE_ENV === 'production'\nconst useMicroCache = process.env.MICRO_CACHE !== 'false'\nconst serverInfo =\n  `express/${require('express/package.json').version} ` +\n  `vue-server-renderer/${require('vue-server-renderer/package.json').version}`\n\nconst app = express()\n\nfunction createRenderer (bundle, options) {\n  // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer\n  return createBundleRenderer(bundle, Object.assign(options, {\n    // for component caching\n    cache: LRU({\n      max: 1000,\n      maxAge: 1000 * 60 * 15\n    }),\n    // this is only needed when vue-server-renderer is npm-linked\n    basedir: resolve('./dist'),\n    // recommended for performance\n    runInNewContext: false\n  }))\n}\n\nlet renderer\nlet readyPromise\nconst templatePath = resolve('./src/index.template.html')\nif (isProd) {\n  // In production: create server renderer using template and built server bundle.\n  // The server bundle is generated by vue-ssr-webpack-plugin.\n  const template = fs.readFileSync(templatePath, 'utf-8')\n  const bundle = require('./dist/vue-ssr-server-bundle.json')\n  // The client manifests are optional, but it allows the renderer\n  // to automatically infer preload/prefetch links and directly add <script>\n  // tags for any async chunks used during render, avoiding waterfall requests.\n  const clientManifest = require('./dist/vue-ssr-client-manifest.json')\n  renderer = createRenderer(bundle, {\n    template,\n    clientManifest\n  })\n} else {\n  // In development: setup the dev server with watch and hot-reload,\n  // and create a new renderer on bundle / index template update.\n  readyPromise = require('./build/setup-dev-server')(\n    app,\n    templatePath,\n    (bundle, options) => {\n      renderer = createRenderer(bundle, options)\n    }\n  )\n}\n\nconst serve = (path, cache) => express.static(resolve(path), {\n  maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0\n})\n\napp.use(compression({ threshold: 0 }))\napp.use(favicon('./public/logo-48.png'))\napp.use('/dist', serve('./dist', true))\napp.use('/public', serve('./public', true))\napp.use('/manifest.json', serve('./manifest.json', true))\napp.use('/service-worker.js', serve('./dist/service-worker.js'))\n\n// since this app has no user-specific content, every page is micro-cacheable.\n// if your app involves user-specific content, you need to implement custom\n// logic to determine whether a request is cacheable based on its url and\n// headers.\n// 1-second microcache.\n// https://www.nginx.com/blog/benefits-of-microcaching-nginx/\napp.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl))\n\nfunction render (req, res) {\n  const s = Date.now()\n\n  res.setHeader(\"Content-Type\", \"text/html\")\n  res.setHeader(\"Server\", serverInfo)\n\n  const handleError = err => {\n    if (err.url) {\n      res.redirect(err.url)\n    } else if(err.code === 404) {\n      res.status(404).send('404 | Page Not Found')\n    } else {\n      // Render Error Page or Redirect\n      res.status(500).send('500 | Internal Server Error')\n      console.error(`error during render : ${req.url}`)\n      console.error(err.stack)\n    }\n  }\n\n  const context = {\n    title: 'Vue HN 2.0', // default title\n    url: req.url\n  }\n  renderer.renderToString(context, (err, html) => {\n    if (err) {\n      return handleError(err)\n    }\n    res.send(html)\n    if (!isProd) {\n      console.log(`whole request: ${Date.now() - s}ms`)\n    }\n  })\n}\n\napp.get('*', isProd ? render : (req, res) => {\n  readyPromise.then(() => render(req, res))\n})\n\nconst port = process.env.PORT || 8080\napp.listen(port, () => {\n  console.log(`server started at localhost:${port}`)\n})\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <header class=\"header\">\n      <nav class=\"inner\">\n        <router-link to=\"/\" exact>\n          <img class=\"logo\" src=\"~public/logo-48.png\" alt=\"logo\">\n        </router-link>\n        <router-link to=\"/top\">Top</router-link>\n        <router-link to=\"/new\">New</router-link>\n        <router-link to=\"/show\">Show</router-link>\n        <router-link to=\"/ask\">Ask</router-link>\n        <router-link to=\"/job\">Jobs</router-link>\n        <a class=\"github\" href=\"https://github.com/vuejs/vue-hackernews-2.0\" target=\"_blank\" rel=\"noopener\">\n          Built with Vue.js\n        </a>\n      </nav>\n    </header>\n    <transition name=\"fade\" mode=\"out-in\">\n      <router-view class=\"view\"></router-view>\n    </transition>\n  </div>\n</template>\n\n<style lang=\"stylus\">\nbody\n  font-family -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n  font-size 15px\n  background-color lighten(#eceef1, 30%)\n  margin 0\n  padding-top 55px\n  color #34495e\n  overflow-y scroll\n\na\n  color #34495e\n  text-decoration none\n\n.header\n  background-color #ff6600\n  position fixed\n  z-index 999\n  height 55px\n  top 0\n  left 0\n  right 0\n  .inner\n    max-width 800px\n    box-sizing border-box\n    margin 0px auto\n    padding 15px 5px\n  a\n    color rgba(255, 255, 255, .8)\n    line-height 24px\n    transition color .15s ease\n    display inline-block\n    vertical-align middle\n    font-weight 300\n    letter-spacing .075em\n    margin-right 1.8em\n    &:hover\n      color #fff\n    &.router-link-active\n      color #fff\n      font-weight 400\n    &:nth-child(6)\n      margin-right 0\n  .github\n    color #fff\n    font-size .9em\n    margin 0\n    float right\n\n.logo\n  width 24px\n  margin-right 10px\n  display inline-block\n  vertical-align middle\n\n.view\n  max-width 800px\n  margin 0 auto\n  position relative\n\n.fade-enter-active, .fade-leave-active\n  transition all .2s ease\n\n.fade-enter, .fade-leave-active\n  opacity 0\n\n@media (max-width 860px)\n  .header .inner\n    padding 15px 30px\n\n@media (max-width 600px)\n  .header\n    .inner\n      padding 15px\n    a\n      margin-right 1em\n    .github\n      display none\n</style>\n"
  },
  {
    "path": "src/api/create-api-client.js",
    "content": "import Firebase from 'firebase/app'\nimport 'firebase/database'\n\nexport function createAPI ({ config, version }) {\n  Firebase.initializeApp(config)\n  return Firebase.database().ref(version)\n}\n"
  },
  {
    "path": "src/api/create-api-server.js",
    "content": "import Firebase from 'firebase'\nimport LRU from 'lru-cache'\n\nexport function createAPI ({ config, version }) {\n  let api\n  // this piece of code may run multiple times in development mode,\n  // so we attach the instantiated API to `process` to avoid duplications\n  if (process.__API__) {\n    api = process.__API__\n  } else {\n    Firebase.initializeApp(config)\n    api = process.__API__ = Firebase.database().ref(version)\n\n    api.onServer = true\n\n    // fetched item cache\n    api.cachedItems = LRU({\n      max: 1000,\n      maxAge: 1000 * 60 * 15 // 15 min cache\n    })\n\n    // cache the latest story ids\n    api.cachedIds = {}\n    ;['top', 'new', 'show', 'ask', 'job'].forEach(type => {\n      api.child(`${type}stories`).on('value', snapshot => {\n        api.cachedIds[type] = snapshot.val()\n      })\n    })\n  }\n  return api\n}\n"
  },
  {
    "path": "src/api/index.js",
    "content": "// this is aliased in webpack config based on server/client build\nimport { createAPI } from 'create-api'\n\nconst logRequests = !!process.env.DEBUG_API\n\nconst api = createAPI({\n  version: '/v0',\n  config: {\n    databaseURL: 'https://hacker-news.firebaseio.com'\n  }\n})\n\n// warm the front page cache every 15 min\n// make sure to do this only once across all requests\nif (api.onServer) {\n  warmCache()\n}\n\nfunction warmCache () {\n  fetchItems((api.cachedIds.top || []).slice(0, 30))\n  setTimeout(warmCache, 1000 * 60 * 15)\n}\n\nfunction fetch (child) {\n  logRequests && console.log(`fetching ${child}...`)\n  const cache = api.cachedItems\n  if (cache && cache.has(child)) {\n    logRequests && console.log(`cache hit for ${child}.`)\n    return Promise.resolve(cache.get(child))\n  } else {\n    return new Promise((resolve, reject) => {\n      api.child(child).once('value', snapshot => {\n        const val = snapshot.val()\n        // mark the timestamp when this item is cached\n        if (val) val.__lastUpdated = Date.now()\n        cache && cache.set(child, val)\n        logRequests && console.log(`fetched ${child}.`)\n        resolve(val)\n      }, reject)\n    })\n  }\n}\n\nexport function fetchIdsByType (type) {\n  return api.cachedIds && api.cachedIds[type]\n    ? Promise.resolve(api.cachedIds[type])\n    : fetch(`${type}stories`)\n}\n\nexport function fetchItem (id) {\n  return fetch(`item/${id}`)\n}\n\nexport function fetchItems (ids) {\n  return Promise.all(ids.map(id => fetchItem(id)))\n}\n\nexport function fetchUser (id) {\n  return fetch(`user/${id}`)\n}\n\nexport function watchList (type, cb) {\n  let first = true\n  const ref = api.child(`${type}stories`)\n  const handler = snapshot => {\n    if (first) {\n      first = false\n    } else {\n      cb(snapshot.val())\n    }\n  }\n  ref.on('value', handler)\n  return () => {\n    ref.off('value', handler)\n  }\n}\n"
  },
  {
    "path": "src/app.js",
    "content": "import Vue from 'vue'\nimport App from './App.vue'\nimport { createStore } from './store'\nimport { createRouter } from './router'\nimport { sync } from 'vuex-router-sync'\nimport titleMixin from './util/title'\nimport * as filters from './util/filters'\n\n// mixin for handling title\nVue.mixin(titleMixin)\n\n// register global utility filters.\nObject.keys(filters).forEach(key => {\n  Vue.filter(key, filters[key])\n})\n\n// Expose a factory function that creates a fresh set of store, router,\n// app instances on each call (which is called for each SSR request)\nexport function createApp () {\n  // create store and router instances\n  const store = createStore()\n  const router = createRouter()\n\n  // sync the router with the vuex store.\n  // this registers `store.state.route`\n  sync(store, router)\n\n  // create the app instance.\n  // here we inject the router, store and ssr context to all child components,\n  // making them available everywhere as `this.$router` and `this.$store`.\n  const app = new Vue({\n    router,\n    store,\n    render: h => h(App)\n  })\n\n  // expose the app, the router and the store.\n  // note we are not mounting the app here, since bootstrapping will be\n  // different depending on whether we are in a browser or on the server.\n  return { app, router, store }\n}\n"
  },
  {
    "path": "src/components/Comment.vue",
    "content": "<template>\n  <li v-if=\"comment\" class=\"comment\">\n    <div class=\"by\">\n      <router-link :to=\"'/user/' + comment.by\">{{ comment.by }}</router-link>\n      {{ comment.time | timeAgo }} ago\n    </div>\n    <div class=\"text\" v-html=\"comment.text\"></div>\n    <div class=\"toggle\" :class=\"{ open }\" v-if=\"comment.kids && comment.kids.length\">\n      <a @click=\"open = !open\">{{\n        open\n            ? '[-]'\n            : '[+] ' + pluralize(comment.kids.length) + ' collapsed'\n      }}</a>\n    </div>\n    <ul class=\"comment-children\" v-show=\"open\">\n      <comment v-for=\"id in comment.kids\" :key=\"id\" :id=\"id\"></comment>\n    </ul>\n  </li>\n</template>\n\n<script>\nexport default {\n  name: 'comment',\n  props: ['id'],\n  data () {\n    return {\n      open: true\n    }\n  },\n  computed: {\n    comment () {\n      return this.$store.state.items[this.id]\n    }\n  },\n  methods: {\n    pluralize: n => n + (n === 1 ? ' reply' : ' replies')\n  }\n}\n</script>\n\n<style lang=\"stylus\">\n.comment-children\n  .comment-children\n    margin-left 1.5em\n\n.comment\n  border-top 1px solid #eee\n  position relative\n  .by, .text, .toggle\n    font-size .9em\n    margin 1em 0\n  .by\n    color #828282\n    a\n      color #828282\n      text-decoration underline\n  .text\n    overflow-wrap break-word\n    a:hover\n      color #ff6600\n    pre\n      white-space pre-wrap\n  .toggle\n    background-color #fffbf2\n    padding .3em .5em\n    border-radius 4px\n    a\n      color #828282\n      cursor pointer\n    &.open\n      padding 0\n      background-color transparent\n      margin-bottom -0.5em\n</style>\n"
  },
  {
    "path": "src/components/Item.vue",
    "content": "<template>\n  <li class=\"news-item\">\n    <span class=\"score\">{{ item.score }}</span>\n    <span class=\"title\">\n      <template v-if=\"item.url\">\n        <a :href=\"item.url\" target=\"_blank\" rel=\"noopener\">{{ item.title }}</a>\n        <span class=\"host\"> ({{ item.url | host }})</span>\n      </template>\n      <template v-else>\n        <router-link :to=\"'/item/' + item.id\">{{ item.title }}</router-link>\n      </template>\n    </span>\n    <br>\n    <span class=\"meta\">\n      <span v-if=\"item.type !== 'job'\" class=\"by\">\n        by <router-link :to=\"'/user/' + item.by\">{{ item.by }}</router-link>\n      </span>\n      <span class=\"time\">\n        {{ item.time | timeAgo }} ago\n      </span>\n      <span v-if=\"item.type !== 'job'\" class=\"comments-link\">\n        | <router-link :to=\"'/item/' + item.id\">{{ item.descendants }} comments</router-link>\n      </span>\n    </span>\n    <span class=\"label\" v-if=\"item.type !== 'story'\">{{ item.type }}</span>\n  </li>\n</template>\n\n<script>\nimport { timeAgo } from '../util/filters'\n\nexport default {\n  name: 'news-item',\n  props: ['item'],\n  // http://ssr.vuejs.org/en/caching.html#component-level-caching\n  serverCacheKey: ({ item: { id, __lastUpdated, time }}) => {\n    return `${id}::${__lastUpdated}::${timeAgo(time)}`\n  }\n}\n</script>\n\n<style lang=\"stylus\">\n.news-item\n  background-color #fff\n  padding 20px 30px 20px 80px\n  border-bottom 1px solid #eee\n  position relative\n  line-height 20px\n  .score\n    color #ff6600\n    font-size 1.1em\n    font-weight 700\n    position absolute\n    top 50%\n    left 0\n    width 80px\n    text-align center\n    margin-top -10px\n  .meta, .host\n    font-size .85em\n    color #828282\n    a\n      color #828282\n      text-decoration underline\n      &:hover\n        color #ff6600\n</style>\n"
  },
  {
    "path": "src/components/ProgressBar.vue",
    "content": "<!-- borrowed from Nuxt! -->\n\n<template>\n  <div class=\"progress\" :style=\"{\n    'width': percent+'%',\n    'height': height,\n    'background-color': canSuccess? color : failedColor,\n    'opacity': show ? 1 : 0\n  }\"></div>\n</template>\n\n<script>\nexport default {\n  data () {\n    return {\n      percent: 0,\n      show: false,\n      canSuccess: true,\n      duration: 3000,\n      height: '2px',\n      color: '#ffca2b',\n      failedColor: '#ff0000',\n    }\n  },\n  methods: {\n    start () {\n      this.show = true\n      this.canSuccess = true\n      if (this._timer) {\n        clearInterval(this._timer)\n        this.percent = 0\n      }\n      this._cut = 10000 / Math.floor(this.duration)\n      this._timer = setInterval(() => {\n        this.increase(this._cut * Math.random())\n        if (this.percent > 95) {\n          this.finish()\n        }\n      }, 100)\n      return this\n    },\n    set (num) {\n      this.show = true\n      this.canSuccess = true\n      this.percent = Math.floor(num)\n      return this\n    },\n    get () {\n      return Math.floor(this.percent)\n    },\n    increase (num) {\n      this.percent = this.percent + Math.floor(num)\n      return this\n    },\n    decrease (num) {\n      this.percent = this.percent - Math.floor(num)\n      return this\n    },\n    finish () {\n      this.percent = 100\n      this.hide()\n      return this\n    },\n    pause () {\n      clearInterval(this._timer)\n      return this\n    },\n    hide () {\n      clearInterval(this._timer)\n      this._timer = null\n      setTimeout(() => {\n        this.show = false\n        this.$nextTick(() => {\n          setTimeout(() => {\n            this.percent = 0\n          }, 200)\n        })\n      }, 500)\n      return this\n    },\n    fail () {\n      this.canSuccess = false\n      return this\n    }\n  }\n}\n</script>\n\n<style lang=\"stylus\" scoped>\n.progress\n  position: fixed\n  top: 0px\n  left: 0px\n  right: 0px\n  height: 2px\n  width: 0%\n  transition: width 0.2s, opacity 0.4s\n  opacity: 1\n  background-color: #efc14e\n  z-index: 999999\n</style>\n"
  },
  {
    "path": "src/components/Spinner.vue",
    "content": "<template>\n  <transition>\n    <svg class=\"spinner\" :class=\"{ show: show }\" v-show=\"show\" width=\"44px\" height=\"44px\" viewBox=\"0 0 44 44\">\n      <circle class=\"path\" fill=\"none\" stroke-width=\"4\" stroke-linecap=\"round\" cx=\"22\" cy=\"22\" r=\"20\"></circle>\n    </svg>\n  </transition>\n</template>\n\n<script>\nexport default {\n  name: 'spinner',\n  props: ['show'],\n  serverCacheKey: props => props.show\n}\n</script>\n\n<style lang=\"stylus\">\n$offset = 126\n$duration = 1.4s\n\n.spinner\n  transition opacity .15s ease\n  animation rotator $duration linear infinite\n  animation-play-state paused\n  &.show\n    animation-play-state running\n  &.v-enter, &.v-leave-active\n    opacity 0\n  &.v-enter-active, &.v-leave\n    opacity 1\n\n@keyframes rotator\n  0%\n    transform scale(0.5) rotate(0deg)\n  100%\n    transform scale(0.5) rotate(270deg)\n\n.spinner .path\n  stroke #ff6600\n  stroke-dasharray $offset\n  stroke-dashoffset 0\n  transform-origin center\n  animation dash $duration ease-in-out infinite\n\n@keyframes dash\n  0%\n    stroke-dashoffset $offset\n  50%\n    stroke-dashoffset ($offset/2)\n    transform rotate(135deg)\n  100%\n    stroke-dashoffset $offset\n    transform rotate(450deg)\n</style>\n"
  },
  {
    "path": "src/entry-client.js",
    "content": "import Vue from 'vue'\nimport 'es6-promise/auto'\nimport { createApp } from './app'\nimport ProgressBar from './components/ProgressBar.vue'\n\n// global progress bar\nconst bar = Vue.prototype.$bar = new Vue(ProgressBar).$mount()\ndocument.body.appendChild(bar.$el)\n\n// a global mixin that calls `asyncData` when a route component's params change\nVue.mixin({\n  beforeRouteUpdate (to, from, next) {\n    const { asyncData } = this.$options\n    if (asyncData) {\n      asyncData({\n        store: this.$store,\n        route: to\n      }).then(next).catch(next)\n    } else {\n      next()\n    }\n  }\n})\n\nconst { app, router, store } = createApp()\n\n// prime the store with server-initialized state.\n// the state is determined during SSR and inlined in the page markup.\nif (window.__INITIAL_STATE__) {\n  store.replaceState(window.__INITIAL_STATE__)\n}\n\n// wait until router has resolved all async before hooks\n// and async components...\nrouter.onReady(() => {\n  // Add router hook for handling asyncData.\n  // Doing it after initial route is resolved so that we don't double-fetch\n  // the data that we already have. Using router.beforeResolve() so that all\n  // async components are resolved.\n  router.beforeResolve((to, from, next) => {\n    const matched = router.getMatchedComponents(to)\n    const prevMatched = router.getMatchedComponents(from)\n    let diffed = false\n    const activated = matched.filter((c, i) => {\n      return diffed || (diffed = (prevMatched[i] !== c))\n    })\n    const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)\n    if (!asyncDataHooks.length) {\n      return next()\n    }\n\n    bar.start()\n    Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))\n      .then(() => {\n        bar.finish()\n        next()\n      })\n      .catch(next)\n  })\n\n  // actually mount to DOM\n  app.$mount('#app')\n})\n\n// service worker\nif ('https:' === location.protocol && navigator.serviceWorker) {\n  navigator.serviceWorker.register('/service-worker.js')\n}\n"
  },
  {
    "path": "src/entry-server.js",
    "content": "import { createApp } from './app'\n\nconst isDev = process.env.NODE_ENV !== 'production'\n\n// This exported function will be called by `bundleRenderer`.\n// This is where we perform data-prefetching to determine the\n// state of our application before actually rendering it.\n// Since data fetching is async, this function is expected to\n// return a Promise that resolves to the app instance.\nexport default context => {\n  return new Promise((resolve, reject) => {\n    const s = isDev && Date.now()\n    const { app, router, store } = createApp()\n\n    const { url } = context\n    const { fullPath } = router.resolve(url).route\n\n    if (fullPath !== url) {\n      return reject({ url: fullPath })\n    }\n\n    // set router's location\n    router.push(url)\n\n    // wait until router has resolved possible async hooks\n    router.onReady(() => {\n      const matchedComponents = router.getMatchedComponents()\n      // no matched routes\n      if (!matchedComponents.length) {\n        return reject({ code: 404 })\n      }\n      // Call fetchData hooks on components matched by the route.\n      // A preFetch hook dispatches a store action and returns a Promise,\n      // which is resolved when the action is complete and store state has been\n      // updated.\n      Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({\n        store,\n        route: router.currentRoute\n      }))).then(() => {\n        isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)\n        // After all preFetch hooks are resolved, our store is now\n        // filled with the state needed to render the app.\n        // Expose the state on the render context, and let the request handler\n        // inline the state in the HTML response. This allows the client-side\n        // store to pick-up the server-side state without having to duplicate\n        // the initial data fetching on the client.\n        context.state = store.state\n        resolve(app)\n      }).catch(reject)\n    }, reject)\n  })\n}\n"
  },
  {
    "path": "src/index.template.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <title>{{ title }}</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\">\n    <link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"/public/logo-120.png\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimal-ui\">\n    <link rel=\"shortcut icon\" sizes=\"48x48\" href=\"/public/logo-48.png\">\n    <meta name=\"theme-color\" content=\"#f60\">\n    <link rel=\"manifest\" href=\"/manifest.json\">\n    <style>\n      #skip a {  position:absolute;  left:-10000px;  top:auto;  width:1px;  height:1px;  overflow:hidden;  }\n      #skip a:focus {  position:static;  width:auto;  height:auto;  }\n    </style>\n  </head>\n  <body>\n  <div id=\"skip\"><a href=\"#app\">skip to content</a></div>\n  <!--vue-ssr-outlet-->\n  </body>\n</html>\n"
  },
  {
    "path": "src/router/index.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\n// route-level code splitting\nconst createListView = id => () => import('../views/CreateListView').then(m => m.default(id))\nconst ItemView = () => import('../views/ItemView.vue')\nconst UserView = () => import('../views/UserView.vue')\n\nexport function createRouter () {\n  return new Router({\n    mode: 'history',\n    fallback: false,\n    scrollBehavior: () => ({ y: 0 }),\n    routes: [\n      { path: '/top/:page(\\\\d+)?', component: createListView('top') },\n      { path: '/new/:page(\\\\d+)?', component: createListView('new') },\n      { path: '/show/:page(\\\\d+)?', component: createListView('show') },\n      { path: '/ask/:page(\\\\d+)?', component: createListView('ask') },\n      { path: '/job/:page(\\\\d+)?', component: createListView('job') },\n      { path: '/item/:id(\\\\d+)', component: ItemView },\n      { path: '/user/:id', component: UserView },\n      { path: '/', redirect: '/top' }\n    ]\n  })\n}\n"
  },
  {
    "path": "src/store/actions.js",
    "content": "import {\n  fetchUser,\n  fetchItems,\n  fetchIdsByType\n} from '../api'\n\nexport default {\n  // ensure data for rendering given list type\n  FETCH_LIST_DATA: ({ commit, dispatch, state }, { type }) => {\n    commit('SET_ACTIVE_TYPE', { type })\n    return fetchIdsByType(type)\n      .then(ids => commit('SET_LIST', { type, ids }))\n      .then(() => dispatch('ENSURE_ACTIVE_ITEMS'))\n  },\n\n  // ensure all active items are fetched\n  ENSURE_ACTIVE_ITEMS: ({ dispatch, getters }) => {\n    return dispatch('FETCH_ITEMS', {\n      ids: getters.activeIds\n    })\n  },\n\n  FETCH_ITEMS: ({ commit, state }, { ids }) => {\n    // on the client, the store itself serves as a cache.\n    // only fetch items that we do not already have, or has expired (3 minutes)\n    const now = Date.now()\n    ids = ids.filter(id => {\n      const item = state.items[id]\n      if (!item) {\n        return true\n      }\n      if (now - item.__lastUpdated > 1000 * 60 * 3) {\n        return true\n      }\n      return false\n    })\n    if (ids.length) {\n      return fetchItems(ids).then(items => commit('SET_ITEMS', { items }))\n    } else {\n      return Promise.resolve()\n    }\n  },\n\n  FETCH_USER: ({ commit, state }, { id }) => {\n    return state.users[id]\n      ? Promise.resolve(state.users[id])\n      : fetchUser(id).then(user => commit('SET_USER', { id, user }))\n  }\n}\n"
  },
  {
    "path": "src/store/getters.js",
    "content": "export default {\n  // ids of the items that should be currently displayed based on\n  // current list type and current pagination\n  activeIds (state) {\n    const { activeType, itemsPerPage, lists } = state\n\n    if (!activeType) {\n      return []\n    }\n\n    const page = Number(state.route.params.page) || 1\n    const start = (page - 1) * itemsPerPage\n    const end = page * itemsPerPage\n\n    return lists[activeType].slice(start, end)\n  },\n\n  // items that should be currently displayed.\n  // this Array may not be fully fetched.\n  activeItems (state, getters) {\n    return getters.activeIds.map(id => state.items[id]).filter(_ => _)\n  }\n}\n"
  },
  {
    "path": "src/store/index.js",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport actions from './actions'\nimport mutations from './mutations'\nimport getters from './getters'\n\nVue.use(Vuex)\n\nexport function createStore () {\n  return new Vuex.Store({\n    state: {\n      activeType: null,\n      itemsPerPage: 20,\n      items: {/* [id: number]: Item */},\n      users: {/* [id: string]: User */},\n      lists: {\n        top: [/* number */],\n        new: [],\n        show: [],\n        ask: [],\n        job: []\n      }\n    },\n    actions,\n    mutations,\n    getters\n  })\n}\n"
  },
  {
    "path": "src/store/mutations.js",
    "content": "import Vue from 'vue'\n\nexport default {\n  SET_ACTIVE_TYPE: (state, { type }) => {\n    state.activeType = type\n  },\n\n  SET_LIST: (state, { type, ids }) => {\n    state.lists[type] = ids\n  },\n\n  SET_ITEMS: (state, { items }) => {\n    items.forEach(item => {\n      if (item) {\n        Vue.set(state.items, item.id, item)\n      }\n    })\n  },\n\n  SET_USER: (state, { id, user }) => {\n    Vue.set(state.users, id, user || false) /* false means user not found */\n  }\n}\n"
  },
  {
    "path": "src/util/filters.js",
    "content": "export function host (url) {\n  const host = url.replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '')\n  const parts = host.split('.').slice(-3)\n  if (parts[0] === 'www') parts.shift()\n  return parts.join('.')\n}\n\nexport function timeAgo (time) {\n  const between = Date.now() / 1000 - Number(time)\n  if (between < 3600) {\n    return pluralize(~~(between / 60), ' minute')\n  } else if (between < 86400) {\n    return pluralize(~~(between / 3600), ' hour')\n  } else {\n    return pluralize(~~(between / 86400), ' day')\n  }\n}\n\nfunction pluralize (time, label) {\n  if (time === 1) {\n    return time + label\n  }\n  return time + label + 's'\n}\n"
  },
  {
    "path": "src/util/title.js",
    "content": "function getTitle (vm) {\n  const { title } = vm.$options\n  if (title) {\n    return typeof title === 'function'\n      ? title.call(vm)\n      : title\n  }\n}\n\nconst serverTitleMixin = {\n  created () {\n    const title = getTitle(this)\n    if (title) {\n      this.$ssrContext.title = `Vue HN 2.0 | ${title}`\n    }\n  }\n}\n\nconst clientTitleMixin = {\n  mounted () {\n    const title = getTitle(this)\n    if (title) {\n      document.title = `Vue HN 2.0 | ${title}`\n    }\n  }\n}\n\nexport default process.env.VUE_ENV === 'server'\n  ? serverTitleMixin\n  : clientTitleMixin\n"
  },
  {
    "path": "src/views/CreateListView.js",
    "content": "import ItemList from './ItemList.vue'\n\nconst camelize = str => str.charAt(0).toUpperCase() + str.slice(1)\n\n// This is a factory function for dynamically creating root-level list views,\n// since they share most of the logic except for the type of items to display.\n// They are essentially higher order components wrapping ItemList.vue.\nexport default function createListView (type) {\n  return {\n    name: `${type}-stories-view`,\n\n    asyncData ({ store }) {\n      return store.dispatch('FETCH_LIST_DATA', { type })\n    },\n\n    title: camelize(type),\n\n    render (h) {\n      return h(ItemList, { props: { type }})\n    }\n  }\n}\n"
  },
  {
    "path": "src/views/ItemList.vue",
    "content": "<template>\n  <div class=\"news-view\">\n    <div class=\"news-list-nav\">\n      <router-link v-if=\"page > 1\" :to=\"'/' + type + '/' + (page - 1)\">&lt; prev</router-link>\n      <a v-else class=\"disabled\">&lt; prev</a>\n      <span>{{ page }}/{{ maxPage }}</span>\n      <router-link v-if=\"hasMore\" :to=\"'/' + type + '/' + (page + 1)\">more &gt;</router-link>\n      <a v-else class=\"disabled\">more &gt;</a>\n    </div>\n    <transition :name=\"transition\">\n      <div class=\"news-list\" :key=\"displayedPage\" v-if=\"displayedPage > 0\">\n        <transition-group tag=\"ul\" name=\"item\">\n          <item v-for=\"item in displayedItems\" :key=\"item.id\" :item=\"item\">\n          </item>\n        </transition-group>\n      </div>\n    </transition>\n  </div>\n</template>\n\n<script>\nimport { watchList } from '../api'\nimport Item from '../components/Item.vue'\n\nexport default {\n  name: 'item-list',\n\n  components: {\n    Item\n  },\n\n  props: {\n    type: String\n  },\n\n  data () {\n    return {\n      transition: 'slide-right',\n      displayedPage: Number(this.$route.params.page) || 1,\n      displayedItems: this.$store.getters.activeItems\n    }\n  },\n\n  computed: {\n    page () {\n      return Number(this.$route.params.page) || 1\n    },\n    maxPage () {\n      const { itemsPerPage, lists } = this.$store.state\n      return Math.ceil(lists[this.type].length / itemsPerPage)\n    },\n    hasMore () {\n      return this.page < this.maxPage\n    }\n  },\n\n  beforeMount () {\n    if (this.$root._isMounted) {\n      this.loadItems(this.page)\n    }\n    // watch the current list for realtime updates\n    this.unwatchList = watchList(this.type, ids => {\n      this.$store.commit('SET_LIST', { type: this.type, ids })\n      this.$store.dispatch('ENSURE_ACTIVE_ITEMS').then(() => {\n        this.displayedItems = this.$store.getters.activeItems\n      })\n    })\n  },\n\n  beforeDestroy () {\n    this.unwatchList()\n  },\n\n  watch: {\n    page (to, from) {\n      this.loadItems(to, from)\n    }\n  },\n\n  methods: {\n    loadItems (to = this.page, from = -1) {\n      this.$bar.start()\n      this.$store.dispatch('FETCH_LIST_DATA', {\n        type: this.type\n      }).then(() => {\n        if (this.page < 0 || this.page > this.maxPage) {\n          this.$router.replace(`/${this.type}/1`)\n          return\n        }\n        this.transition = from === -1\n          ? null\n          : to > from ? 'slide-left' : 'slide-right'\n        this.displayedPage = to\n        this.displayedItems = this.$store.getters.activeItems\n        this.$bar.finish()\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"stylus\">\n.news-view\n  padding-top 45px\n\n.news-list-nav, .news-list\n  background-color #fff\n  border-radius 2px\n\n.news-list-nav\n  padding 15px 30px\n  position fixed\n  text-align center\n  top 55px\n  left 0\n  right 0\n  z-index 998\n  box-shadow 0 1px 2px rgba(0,0,0,.1)\n  a\n    margin 0 1em\n  .disabled\n    color #ccc\n\n.news-list\n  position absolute\n  margin 30px 0\n  width 100%\n  transition all .5s cubic-bezier(.55,0,.1,1)\n  ul\n    list-style-type none\n    padding 0\n    margin 0\n\n.slide-left-enter, .slide-right-leave-to\n  opacity 0\n  transform translate(30px, 0)\n\n.slide-left-leave-to, .slide-right-enter\n  opacity 0\n  transform translate(-30px, 0)\n\n.item-move, .item-enter-active, .item-leave-active\n  transition all .5s cubic-bezier(.55,0,.1,1)\n\n.item-enter\n  opacity 0\n  transform translate(30px, 0)\n\n.item-leave-active\n  position absolute\n  opacity 0\n  transform translate(30px, 0)\n\n@media (max-width 600px)\n  .news-list\n    margin 10px 0\n</style>\n"
  },
  {
    "path": "src/views/ItemView.vue",
    "content": "<template>\n  <div class=\"item-view\" v-if=\"item\">\n    <template v-if=\"item\">\n      <div class=\"item-view-header\">\n        <a :href=\"item.url\" target=\"_blank\">\n          <h1>{{ item.title }}</h1>\n        </a>\n        <span v-if=\"item.url\" class=\"host\">\n          ({{ item.url | host }})\n        </span>\n        <p class=\"meta\">\n          {{ item.score }} points\n          | by <router-link :to=\"'/user/' + item.by\">{{ item.by }}</router-link>\n          {{ item.time | timeAgo }} ago\n        </p>\n      </div>\n      <div class=\"item-view-comments\">\n        <p class=\"item-view-comments-header\">\n          {{ item.kids ? item.descendants + ' comments' : 'No comments yet.' }}\n          <spinner :show=\"loading\"></spinner>\n        </p>\n        <ul v-if=\"!loading\" class=\"comment-children\">\n          <comment v-for=\"id in item.kids\" :key=\"id\" :id=\"id\"></comment>\n        </ul>\n      </div>\n    </template>\n  </div>\n</template>\n\n<script>\nimport Spinner from '../components/Spinner.vue'\nimport Comment from '../components/Comment.vue'\n\nexport default {\n  name: 'item-view',\n  components: { Spinner, Comment },\n\n  data: () => ({\n    loading: true\n  }),\n\n  computed: {\n    item () {\n      return this.$store.state.items[this.$route.params.id]\n    }\n  },\n\n  // We only fetch the item itself before entering the view, because\n  // it might take a long time to load threads with hundreds of comments\n  // due to how the HN Firebase API works.\n  asyncData ({ store, route: { params: { id }}}) {\n    return store.dispatch('FETCH_ITEMS', { ids: [id] })\n  },\n\n  title () {\n    return this.item.title\n  },\n\n  // Fetch comments when mounted on the client\n  beforeMount () {\n    this.fetchComments()\n  },\n\n  // refetch comments if item changed\n  watch: {\n    item: 'fetchComments'\n  },\n\n  methods: {\n    fetchComments () {\n      if (!this.item || !this.item.kids) {\n        return\n      }\n\n      this.loading = true\n      fetchComments(this.$store, this.item).then(() => {\n        this.loading = false\n      })\n    }\n  }\n}\n\n// recursively fetch all descendent comments\nfunction fetchComments (store, item) {\n  if (item && item.kids) {\n    return store.dispatch('FETCH_ITEMS', {\n      ids: item.kids\n    }).then(() => Promise.all(item.kids.map(id => {\n      return fetchComments(store, store.state.items[id])\n    })))\n  }\n}\n</script>\n\n<style lang=\"stylus\">\n.item-view-header\n  background-color #fff\n  padding 1.8em 2em 1em\n  box-shadow 0 1px 2px rgba(0,0,0,.1)\n  h1\n    display inline\n    font-size 1.5em\n    margin 0\n    margin-right .5em\n  .host, .meta, .meta a\n    color #828282\n  .meta a\n    text-decoration underline\n\n.item-view-comments\n  background-color #fff\n  margin-top 10px\n  padding 0 2em .5em\n\n.item-view-comments-header\n  margin 0\n  font-size 1.1em\n  padding 1em 0\n  position relative\n  .spinner\n    display inline-block\n    margin -15px 0\n\n.comment-children\n  list-style-type none\n  padding 0\n  margin 0\n\n@media (max-width 600px)\n  .item-view-header\n    h1\n      font-size 1.25em\n</style>\n"
  },
  {
    "path": "src/views/UserView.vue",
    "content": "<template>\n  <div class=\"user-view\">\n    <template v-if=\"user\">\n      <h1>User : {{ user.id }}</h1>\n      <ul class=\"meta\">\n        <li><span class=\"label\">Created:</span> {{ user.created | timeAgo }} ago</li>\n        <li><span class=\"label\">Karma:</span> {{ user.karma }}</li>\n        <li v-if=\"user.about\" v-html=\"user.about\" class=\"about\"></li>\n      </ul>\n      <p class=\"links\">\n        <a :href=\"'https://news.ycombinator.com/submitted?id=' + user.id\">submissions</a> |\n        <a :href=\"'https://news.ycombinator.com/threads?id=' + user.id\">comments</a>\n      </p>\n    </template>\n    <template v-else-if=\"user === false\">\n      <h1>User not found.</h1>\n    </template>\n  </div>\n</template>\n\n<script>\n\nexport default {\n  name: 'user-view',\n\n  computed: {\n    user () {\n      return this.$store.state.users[this.$route.params.id]\n    }\n  },\n\n  asyncData ({ store, route: { params: { id }}}) {\n    return store.dispatch('FETCH_USER', { id })\n  },\n\n  title () {\n    return this.user\n      ? this.user.id\n      : 'User not found'\n  }\n}\n</script>\n\n<style lang=\"stylus\">\n.user-view\n  background-color #fff\n  box-sizing border-box\n  padding 2em 3em\n  h1\n    margin 0\n    font-size 1.5em\n  .meta\n    list-style-type none\n    padding 0\n  .label\n    display inline-block\n    min-width 4em\n  .about\n    margin 1em 0\n  .links a\n    text-decoration underline\n</style>\n"
  }
]