├── .gitignore ├── README.md ├── build ├── setup-dev-server.js ├── vue-loader.config.js ├── webpack.base.config.js ├── webpack.client.config.js └── webpack.server.config.js ├── dist ├── service-worker.js ├── vendor.87c5eed709b22da7e20a.js ├── vue-ssr-client-manifest.json └── vue-ssr-server-bundle.json ├── manifest.json ├── package.json ├── public ├── logo-120.png ├── logo-144.png ├── logo-152.png ├── logo-192.png ├── logo-256.png ├── logo-384.png ├── logo-48.png └── logo-512.png ├── server.js ├── src ├── App.vue ├── api │ ├── create-api-client.ts │ ├── create-api-server.ts │ └── index.ts ├── app.ts ├── components │ ├── Comment.vue │ ├── Item.vue │ ├── ProgressBar.vue │ └── Spinner.vue ├── entry-client.ts ├── entry-server.ts ├── index.template.html ├── public │ ├── logo-120.png │ ├── logo-144.png │ ├── logo-152.png │ ├── logo-192.png │ ├── logo-256.png │ ├── logo-384.png │ ├── logo-48.png │ └── logo-512.png ├── router │ └── index.ts ├── store │ ├── actions.ts │ ├── getters.ts │ ├── index.ts │ └── mutations.ts ├── util │ ├── filters.ts │ └── title.ts ├── views │ ├── CreateListView.ts │ ├── ItemList.vue │ ├── ItemView.vue │ └── UserView.vue └── vue-shims.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | .idea 7 | *.iml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-hackernews-2.0 with TypeScript 2 | 3 | HackerNews clone built with TypeScript + Vue 2.0 + vue-router + vuex, with server-side rendering. 4 | 5 |

6 | 7 | 8 |
9 | Live Demo 10 |
11 |

12 | 13 | ## Features 14 | 15 | > 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. In real apps, you should always measure and optimize based on your actual app constraints. 16 | 17 | - Server Side Rendering 18 | - Vue + vue-router + vuex working together 19 | - Server-side data pre-fetching 20 | - Client-side state & DOM hydration 21 | - Automatically inlines CSS used by rendered components only 22 | - Preload / prefetch resource hints 23 | - Route-level code splitting 24 | - Progressive Web App 25 | - App manifest 26 | - Service worker 27 | - 100/100 Lighthouse score 28 | - Single-file Vue Components 29 | - Hot-reload in development 30 | - CSS extraction for production 31 | - Animation 32 | - Effects when switching route views 33 | - Real-time list updates with FLIP Animation 34 | 35 | ## Architecture Overview 36 | 37 | screen shot 2016-08-11 at 6 06 57 pm 38 | 39 | **A detailed Vue SSR guide can be found [here](https://ssr.vuejs.org).** 40 | 41 | ## Build Setup 42 | 43 | **Requires Node.js 7+** 44 | 45 | ``` bash 46 | # install dependencies 47 | npm install # or yarn 48 | 49 | # serve in dev mode, with hot reload at localhost:8080 50 | npm run dev 51 | 52 | # build for production 53 | npm run build 54 | 55 | # serve in production mode 56 | npm start 57 | ``` 58 | 59 | ## License 60 | 61 | MIT 62 | -------------------------------------------------------------------------------- /build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const clientConfig = require('./webpack.client.config') 7 | const serverConfig = require('./webpack.server.config') 8 | 9 | const readFile = (fs, file) => { 10 | try { 11 | return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 12 | } catch (e) {} 13 | } 14 | 15 | module.exports = function setupDevServer (app, templatePath, cb) { 16 | let bundle 17 | let template 18 | let clientManifest 19 | 20 | let ready 21 | const readyPromise = new Promise(r => { ready = r }) 22 | const update = () => { 23 | if (bundle && clientManifest) { 24 | ready() 25 | cb(bundle, { 26 | template, 27 | clientManifest 28 | }) 29 | } 30 | } 31 | 32 | // read template from disk and watch 33 | template = fs.readFileSync(templatePath, 'utf-8') 34 | chokidar.watch(templatePath).on('change', () => { 35 | template = fs.readFileSync(templatePath, 'utf-8') 36 | console.log('index.html template updated.') 37 | update() 38 | }) 39 | 40 | // modify client config to work with hot middleware 41 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 42 | clientConfig.output.filename = '[name].js' 43 | clientConfig.plugins.push( 44 | new webpack.HotModuleReplacementPlugin(), 45 | new webpack.NoEmitOnErrorsPlugin() 46 | ) 47 | 48 | // dev middleware 49 | const clientCompiler = webpack(clientConfig) 50 | const devMiddleware = require('webpack-dev-middleware')(clientCompiler, { 51 | publicPath: clientConfig.output.publicPath, 52 | noInfo: true 53 | }) 54 | app.use(devMiddleware) 55 | clientCompiler.plugin('done', stats => { 56 | stats = stats.toJson() 57 | stats.errors.forEach(err => console.error(err)) 58 | stats.warnings.forEach(err => console.warn(err)) 59 | if (stats.errors.length) return 60 | clientManifest = JSON.parse(readFile( 61 | devMiddleware.fileSystem, 62 | 'vue-ssr-client-manifest.json' 63 | )) 64 | update() 65 | }) 66 | 67 | // hot middleware 68 | app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 })) 69 | 70 | // watch and update server renderer 71 | const serverCompiler = webpack(serverConfig) 72 | const mfs = new MFS() 73 | serverCompiler.outputFileSystem = mfs 74 | serverCompiler.watch({}, (err, stats) => { 75 | if (err) throw err 76 | stats = stats.toJson() 77 | if (stats.errors.length) return 78 | 79 | // read bundle generated by vue-ssr-webpack-plugin 80 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 81 | update() 82 | }) 83 | 84 | return readyPromise 85 | } 86 | -------------------------------------------------------------------------------- /build/vue-loader.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | loaders: { 3 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 4 | // the "scss" and "sass" values for the lang attribute to the right configs here. 5 | // other preprocessors should work out of the box, no loader config like this necessary. 6 | 'scss': 'vue-style-loader!css-loader!sass-loader', 7 | 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax', 8 | }, 9 | // other vue-loader options go here 10 | // from vue config 11 | extractCSS: process.env.NODE_ENV === 'production', 12 | preserveWhitespace: false, 13 | postcss: [ 14 | require('autoprefixer')({ 15 | browsers: ['last 3 versions'] 16 | }) 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const vueConfig = require('./vue-loader.config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 6 | 7 | const isProd = process.env.NODE_ENV === 'production' 8 | 9 | module.exports = { 10 | devtool: isProd 11 | ? false 12 | : '#cheap-module-source-map', 13 | output: { 14 | path: path.resolve(__dirname, '../dist'), 15 | publicPath: '/dist/', 16 | filename: '[name].[chunkhash].js' 17 | }, 18 | resolve: { 19 | alias: { 20 | 'public': path.resolve(__dirname, '../public') 21 | } 22 | }, 23 | module: { 24 | noParse: /es6-promise\.js$/, // avoid webpack shimming process 25 | rules: [ 26 | { 27 | test: /\.vue$/, 28 | loader: 'vue-loader', 29 | options: vueConfig 30 | }, 31 | { 32 | test: /\.tsx?$/, 33 | loader: 'ts-loader', 34 | exclude: /node_modules/, 35 | options: { 36 | appendTsSuffixTo: [/\.vue$/], 37 | } 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'babel-loader', 42 | exclude: /node_modules/ 43 | }, 44 | { 45 | test: /\.(png|jpg|gif|svg)$/, 46 | loader: 'url-loader', 47 | options: { 48 | limit: 10000, 49 | name: '[name].[ext]?[hash]' 50 | } 51 | }, 52 | { 53 | test: /\.css$/, 54 | use: isProd 55 | ? ExtractTextPlugin.extract({ 56 | use: 'css-loader?minimize', 57 | fallback: 'vue-style-loader' 58 | }) 59 | : ['vue-style-loader', 'css-loader'] 60 | } 61 | ] 62 | }, 63 | resolve: { 64 | extensions: ['.ts', '.js', '.vue', '.json'], 65 | alias: { 66 | 'vue$': 'vue/dist/vue.esm.js' 67 | } 68 | }, 69 | performance: { 70 | maxEntrypointSize: 300000, 71 | hints: isProd ? 'warning' : false 72 | }, 73 | plugins: isProd 74 | ? [ 75 | new webpack.optimize.UglifyJsPlugin({ 76 | compress: { warnings: false } 77 | }), 78 | new webpack.optimize.ModuleConcatenationPlugin(), 79 | new ExtractTextPlugin({ 80 | filename: 'common.[chunkhash].css' 81 | }) 82 | ] 83 | : [ 84 | new FriendlyErrorsPlugin() 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const SWPrecachePlugin = require('sw-precache-webpack-plugin') 5 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 6 | 7 | const config = merge(base, { 8 | entry: { 9 | app: './src/entry-client.ts' 10 | }, 11 | resolve: { 12 | alias: { 13 | 'create-api': './create-api-client.ts' 14 | } 15 | }, 16 | plugins: [ 17 | // strip dev-only code in Vue source 18 | new webpack.DefinePlugin({ 19 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 20 | 'process.env.VUE_ENV': '"client"' 21 | }), 22 | // extract vendor chunks for better caching 23 | new webpack.optimize.CommonsChunkPlugin({ 24 | name: 'vendor', 25 | minChunks: function (module) { 26 | // a module is extracted into the vendor chunk if... 27 | return ( 28 | // it's inside node_modules 29 | /node_modules/.test(module.context) && 30 | // and not a CSS file (due to extract-text-webpack-plugin limitation) 31 | !/\.css$/.test(module.request) 32 | ) 33 | } 34 | }), 35 | // extract webpack runtime & manifest to avoid vendor chunk hash changing 36 | // on every build. 37 | new webpack.optimize.CommonsChunkPlugin({ 38 | name: 'manifest' 39 | }), 40 | new VueSSRClientPlugin() 41 | ] 42 | }) 43 | 44 | if (process.env.NODE_ENV === 'production') { 45 | config.plugins.push( 46 | // auto generate service worker 47 | new SWPrecachePlugin({ 48 | cacheId: 'vue-hn', 49 | filename: 'service-worker.js', 50 | minify: true, 51 | dontCacheBustUrlsMatching: /./, 52 | staticFileGlobsIgnorePatterns: [/\.map$/, /\.json$/], 53 | runtimeCaching: [ 54 | { 55 | urlPattern: '/', 56 | handler: 'networkFirst' 57 | }, 58 | { 59 | urlPattern: /\/(top|new|show|ask|jobs)/, 60 | handler: 'networkFirst' 61 | }, 62 | { 63 | urlPattern: '/item/:id', 64 | handler: 'networkFirst' 65 | }, 66 | { 67 | urlPattern: '/user/:id', 68 | handler: 'networkFirst' 69 | } 70 | ] 71 | }) 72 | ) 73 | } 74 | 75 | module.exports = config 76 | -------------------------------------------------------------------------------- /build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const base = require('./webpack.base.config') 4 | const nodeExternals = require('webpack-node-externals') 5 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 6 | 7 | module.exports = merge(base, { 8 | target: 'node', 9 | devtool: '#source-map', 10 | entry: './src/entry-server.ts', 11 | output: { 12 | filename: 'server-bundle.js', 13 | libraryTarget: 'commonjs2' 14 | }, 15 | resolve: { 16 | alias: { 17 | 'create-api': './create-api-server.ts' 18 | } 19 | }, 20 | // https://webpack.js.org/configuration/externals/#externals 21 | // https://github.com/liady/webpack-node-externals 22 | externals: nodeExternals({ 23 | // do not externalize CSS files in case we need to import it from a dep 24 | whitelist: /\.css$/ 25 | }), 26 | plugins: [ 27 | new webpack.DefinePlugin({ 28 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 29 | 'process.env.VUE_ENV': '"server"' 30 | }), 31 | new VueSSRServerPlugin() 32 | ] 33 | }) 34 | -------------------------------------------------------------------------------- /dist/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}var precacheConfig=[["/dist/0.2f24aad406604a1cb8f6.js","517c26c1e33c0fe622c51593542e1046"],["/dist/1.a394bb868e28da65444d.js","f25dd6bd70766f6aeeb47ea5b2bd0586"],["/dist/2.6162be221be2a6f3574b.js","a7b401488e7db310895bbbe764e589de"],["/dist/app.22a57857d70ec6affd9f.js","6e6f0563098cf868f7605170c1bbcc36"],["/dist/common.22a57857d70ec6affd9f.css","94102f54b8f8a3d2fa68f67390d0e18d"],["/dist/manifest.2a797793b2b38824bdbb.js","7892db841ebe2b2d0b77a28234b41a21"],["/dist/vendor.87c5eed709b22da7e20a.js","b64d3f58de192911d7fd5ba250c9466f"]],cacheName="sw-precache-v3-vue-hn-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){if(!e.redirected)return Promise.resolve(e);return("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})})},createCacheKey=function(e,t,n,r){var o=new URL(e);return r&&o.pathname.match(r)||(o.search+=(o.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),o.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),o=createCacheKey(r,hashParamName,n,/./);return[r.toString(),o]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching),r="index.html";(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),t=urlsToCacheKeys.has(n));0,t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).toolbox=e()}}(function(){return function e(t,n,r){function o(i,s){if(!n[i]){if(!t[i]){var c="function"==typeof require&&require;if(!s&&c)return c(i,!0);if(a)return a(i,!0);var u=new Error("Cannot find module '"+i+"'");throw u.code="MODULE_NOT_FOUND",u}var f=n[i]={exports:{}};t[i][0].call(f.exports,function(e){var n=t[i][1][e];return o(n||e)},f,f.exports,e,t,n,r)}return n[i].exports}for(var a="function"==typeof require&&require,i=0;it.value[s]){var r=t.value[i];u.push(r),h.delete(r),t.continue()}},f.oncomplete=function(){r(u)},f.onabort=o}):Promise.resolve([])}(e,n,r).then(function(n){return function(e,t){return t?new Promise(function(n,r){var o=[],c=e.transaction(a,"readwrite"),u=c.objectStore(a),f=u.index(s),h=f.count();f.count().onsuccess=function(){var e=h.result;e>t&&(f.openCursor().onsuccess=function(n){var r=n.target.result;if(r){var a=r.value[i];o.push(a),u.delete(a),e-o.length>t&&r.continue()}})},c.oncomplete=function(){n(o)},c.onabort=r}):Promise.resolve([])}(e,t).then(function(e){return n.concat(e)})})}}},{}],3:[function(e,t,n){function r(e){return e.reduce(function(e,t){return e.concat(t)},[])}e("serviceworker-cache-polyfill");var o=e("./helpers"),a=e("./router"),i=e("./options");t.exports={fetchListener:function(e){var t=a.match(e.request);t?e.respondWith(t(e.request)):a.default&&"GET"===e.request.method&&0===e.request.url.indexOf("http")&&e.respondWith(a.default(e.request))},activateListener:function(e){o.debug("activate event fired");var t=i.cache.name+"$$$inactive$$$";e.waitUntil(o.renameCache(t,i.cache.name))},installListener:function(e){var t=i.cache.name+"$$$inactive$$$";o.debug("install event fired"),o.debug("creating cache ["+t+"]"),e.waitUntil(o.openCache({cache:{name:t}}).then(function(e){return Promise.all(i.preCacheItems).then(r).then(o.validatePrecacheInput).then(function(t){return o.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}}},{"./helpers":1,"./options":4,"./router":6,"serviceworker-cache-polyfill":16}],4:[function(e,t,n){var r;r=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,t.exports={cache:{name:"$$$toolbox-cache$$$"+r+"$$$",maxAgeSeconds:null,maxEntries:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/}},{}],5:[function(e,t,n){var r=new URL("./",self.location).pathname,o=e("path-to-regexp"),a=function(e,t,n,a){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=r+t),this.keys=[],this.regexp=o(t,this.keys)),this.method=e,this.options=a,this.handler=n};a.prototype.makeHandler=function(e){var t;if(this.regexp){var n=this.regexp.exec(e);t={},this.keys.forEach(function(e,r){t[e.name]=n[r+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},t.exports=a},{"path-to-regexp":15}],6:[function(e,t,n){var r=e("./route"),o=e("./helpers"),a=function(e,t){for(var n=e.entries(),r=n.next(),o=[];!r.done;){new RegExp(r.value[0]).test(t)&&o.push(r.value[1]),r=n.next()}return o},i=function(){this.routes=new Map,this.routes.set(RegExp,new Map),this.default=null};["get","post","put","delete","head","any"].forEach(function(e){i.prototype[e]=function(t,n,r){return this.add(e,t,n,r)}}),i.prototype.add=function(e,t,n,a){a=a||{};var i;t instanceof RegExp?i=RegExp:(i=a.origin||self.location.origin,i=i instanceof RegExp?i.source:function(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}(i)),e=e.toLowerCase();var s=new r(e,t,n,a);this.routes.has(i)||this.routes.set(i,new Map);var c=this.routes.get(i);c.has(e)||c.set(e,new Map);var u=c.get(e),f=s.regexp||s.fullUrlRegExp;u.has(f.source)&&o.debug('"'+t+'" resolves to same regex as existing route.'),u.set(f.source,s)},i.prototype.matchMethod=function(e,t){var n=new URL(t),r=n.origin,o=n.pathname;return this._match(e,a(this.routes,r),o)||this._match(e,[this.routes.get(RegExp)],t)},i.prototype._match=function(e,t,n){if(0===t.length)return null;for(var r=0;r0)return s[0].makeHandler(n)}}return null},i.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},t.exports=new i},{"./helpers":1,"./route":5}],7:[function(e,t,n){var r=e("../options"),o=e("../helpers");t.exports=function(e,t,n){return n=n||{},o.debug("Strategy: cache first ["+e.url+"]",n),o.openCache(n).then(function(t){return t.match(e).then(function(t){var a=n.cache||r.cache,i=Date.now();return o.isResponseFresh(t,a.maxAgeSeconds,i)?t:o.fetchAndCache(e,n)})})}},{"../helpers":1,"../options":4}],8:[function(e,t,n){var r=e("../options"),o=e("../helpers");t.exports=function(e,t,n){return n=n||{},o.debug("Strategy: cache only ["+e.url+"]",n),o.openCache(n).then(function(t){return t.match(e).then(function(e){var t=n.cache||r.cache,a=Date.now();if(o.isResponseFresh(e,t.maxAgeSeconds,a))return e})})}},{"../helpers":1,"../options":4}],9:[function(e,t,n){var r=e("../helpers"),o=e("./cacheOnly");t.exports=function(e,t,n){return r.debug("Strategy: fastest ["+e.url+"]",n),new Promise(function(a,i){var s=!1,c=[],u=function(e){c.push(e.toString()),s?i(new Error('Both cache and network failed: "'+c.join('", "')+'"')):s=!0},f=function(e){e instanceof Response?a(e):u("No result returned")};r.fetchAndCache(e.clone(),n).then(f,u),o(e,t,n).then(f,u)})}},{"../helpers":1,"./cacheOnly":8}],10:[function(e,t,n){t.exports={networkOnly:e("./networkOnly"),networkFirst:e("./networkFirst"),cacheOnly:e("./cacheOnly"),cacheFirst:e("./cacheFirst"),fastest:e("./fastest")}},{"./cacheFirst":7,"./cacheOnly":8,"./fastest":9,"./networkFirst":11,"./networkOnly":12}],11:[function(e,t,n){var r=e("../options"),o=e("../helpers");t.exports=function(e,t,n){var a=(n=n||{}).successResponses||r.successResponses,i=n.networkTimeoutSeconds||r.networkTimeoutSeconds;return o.debug("Strategy: network first ["+e.url+"]",n),o.openCache(n).then(function(t){var s,c,u=[];if(i){var f=new Promise(function(a){s=setTimeout(function(){t.match(e).then(function(e){var t=n.cache||r.cache,i=Date.now(),s=t.maxAgeSeconds;o.isResponseFresh(e,s,i)&&a(e)})},1e3*i)});u.push(f)}var h=o.fetchAndCache(e,n).then(function(e){if(s&&clearTimeout(s),a.test(e.status))return e;throw o.debug("Response was an HTTP error: "+e.statusText,n),c=e,new Error("Bad response")}).catch(function(r){return o.debug("Network or response error, fallback to cache ["+e.url+"]",n),t.match(e).then(function(e){if(e)return e;if(c)return c;throw r})});return u.push(h),Promise.race(u)})}},{"../helpers":1,"../options":4}],12:[function(e,t,n){var r=e("../helpers");t.exports=function(e,t,n){return r.debug("Strategy: network only ["+e.url+"]",n),fetch(e)}},{"../helpers":1}],13:[function(e,t,n){var r=e("./options"),o=e("./router"),a=e("./helpers"),i=e("./strategies"),s=e("./listeners");a.debug("Service Worker Toolbox is loading"),self.addEventListener("install",s.installListener),self.addEventListener("activate",s.activateListener),self.addEventListener("fetch",s.fetchListener),t.exports={networkOnly:i.networkOnly,networkFirst:i.networkFirst,cacheOnly:i.cacheOnly,cacheFirst:i.cacheFirst,fastest:i.fastest,router:o,options:r,cache:a.cache,uncache:a.uncache,precache:a.precache}},{"./helpers":1,"./listeners":3,"./options":4,"./router":6,"./strategies":10}],14:[function(e,t,n){t.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},{}],15:[function(e,t,n){function r(e,t){for(var n,r=[],o=0,a=0,i="",c=t&&t.delimiter||"/";null!=(n=p.exec(e));){var u=n[0],f=n[1],h=n.index;if(i+=e.slice(a,h),a=h+u.length,f)i+=f[1];else{var l=e[a],d=n[2],m=n[3],g=n[4],v=n[5],w=n[6],x=n[7];i&&(r.push(i),i="");var b=null!=d&&null!=l&&l!==d,y="+"===w||"*"===w,E="?"===w||"*"===w,R=n[2]||c,C=g||v;r.push({name:m||o++,prefix:d||"",delimiter:R,optional:E,repeat:y,partial:b,asterisk:!!x,pattern:C?function(e){return e.replace(/([=!:$\/()])/g,"\\$1")}(C):x?".*":"[^"+s(R)+"]+?"})}}return a=46||"Chrome"===n&&r>=50)||(Cache.prototype.addAll=function(e){function t(e){this.name="NetworkError",this.code=19,this.message=e}var n=this;return t.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return e=e.map(function(e){return e instanceof Request?e:String(e)}),Promise.all(e.map(function(e){"string"==typeof e&&(e=new Request(e));var n=new URL(e.url).protocol;if("http:"!==n&&"https:"!==n)throw new t("Invalid scheme");return fetch(e.clone())}))}).then(function(r){if(r.some(function(e){return!e.ok}))throw new t("Incorrect response status");return Promise.all(r.map(function(t,r){return n.put(e[r],t)}))}).then(function(){})},Cache.prototype.add=function(e){return this.addAll([e])})}()},{}]},{},[13])(13)}),toolbox.router.get("/",toolbox.networkFirst,{}),toolbox.router.get(/\/(top|new|show|ask|jobs)/,toolbox.networkFirst,{}),toolbox.router.get("/item/:id",toolbox.networkFirst,{}),toolbox.router.get("/user/:id",toolbox.networkFirst,{}); -------------------------------------------------------------------------------- /dist/vue-ssr-client-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "publicPath": "/dist/", 3 | "all": [ 4 | "0.2f24aad406604a1cb8f6.js", 5 | "1.a394bb868e28da65444d.js", 6 | "2.6162be221be2a6f3574b.js", 7 | "vendor.87c5eed709b22da7e20a.js", 8 | "app.22a57857d70ec6affd9f.js", 9 | "manifest.2a797793b2b38824bdbb.js", 10 | "common.22a57857d70ec6affd9f.css" 11 | ], 12 | "initial": [ 13 | "manifest.2a797793b2b38824bdbb.js", 14 | "vendor.87c5eed709b22da7e20a.js", 15 | "app.22a57857d70ec6affd9f.js" 16 | ], 17 | "async": [ 18 | "0.2f24aad406604a1cb8f6.js", 19 | "1.a394bb868e28da65444d.js", 20 | "2.6162be221be2a6f3574b.js" 21 | ], 22 | "modules": { 23 | "18554856": [ 24 | 3 25 | ], 26 | "26107428": [ 27 | 3 28 | ], 29 | "60681088": [ 30 | 3 31 | ], 32 | "63589835": [ 33 | 3 34 | ], 35 | "64837578": [ 36 | 3 37 | ], 38 | "72321131": [ 39 | 3 40 | ], 41 | "92237446": [ 42 | 3 43 | ], 44 | "63d8a00e": [ 45 | 3 46 | ], 47 | "7488318b": [ 48 | 3 49 | ], 50 | "a1228330": [ 51 | 3 52 | ], 53 | "3211f81a": [ 54 | 3 55 | ], 56 | "595c1b98": [ 57 | 3 58 | ], 59 | "7eaa0333": [ 60 | 3 61 | ], 62 | "f6240d98": [ 63 | 3 64 | ], 65 | "0c0e89ca": [ 66 | 3 67 | ], 68 | "3010b740": [ 69 | 3 70 | ], 71 | "25d9db1e": [ 72 | 3 73 | ], 74 | "3c577b5a": [ 75 | 3 76 | ], 77 | "253dde61": [ 78 | 3 79 | ], 80 | "2dd3a353": [ 81 | 3 82 | ], 83 | "69b8167f": [ 84 | 3 85 | ], 86 | "27da0154": [ 87 | 3 88 | ], 89 | "3c53b216": [ 90 | 3 91 | ], 92 | "6312a17c": [ 93 | 3 94 | ], 95 | "04235362": [ 96 | 3 97 | ], 98 | "f38fefcc": [ 99 | 3 100 | ], 101 | "07605bf4": [ 102 | 3 103 | ], 104 | "943864de": [ 105 | 3 106 | ], 107 | "72c566ee": [ 108 | 3 109 | ], 110 | "1a82c847": [ 111 | 3 112 | ], 113 | "4da1107e": [ 114 | 3 115 | ], 116 | "4acfefde": [ 117 | 3 118 | ], 119 | "8de629d4": [ 120 | 3 121 | ], 122 | "7c16d37f": [ 123 | 3 124 | ], 125 | "ee158976": [ 126 | 3 127 | ], 128 | "5db76d60": [ 129 | 3 130 | ], 131 | "2e2cc7fd": [ 132 | 3 133 | ], 134 | "e3298586": [ 135 | 3 136 | ], 137 | "28b2b3ca": [ 138 | 3 139 | ], 140 | "0f2f8214": [ 141 | 3 142 | ], 143 | "7fa7b23a": [ 144 | 3 145 | ], 146 | "f03f18be": [ 147 | 3 148 | ], 149 | "6c3e7254": [ 150 | 3 151 | ], 152 | "ea7f8e0c": [ 153 | 3 154 | ], 155 | "97b9bd5e": [ 156 | 3 157 | ], 158 | "2216748b": [ 159 | 3 160 | ], 161 | "44f3f8b6": [ 162 | 3 163 | ], 164 | "fb7653ea": [ 165 | 3 166 | ], 167 | "e68143a2": [ 168 | 3 169 | ], 170 | "5e36a3c8": [ 171 | 3 172 | ], 173 | "6546b21b": [ 174 | 3 175 | ], 176 | "4c384370": [ 177 | 3 178 | ], 179 | "4a3de75c": [ 180 | 3 181 | ], 182 | "45514fce": [ 183 | 3 184 | ], 185 | "09b6a312": [ 186 | 3 187 | ], 188 | "0ad593b6": [ 189 | 3 190 | ], 191 | "650eee41": [ 192 | 3 193 | ], 194 | "940c6b52": [ 195 | 3 196 | ], 197 | "19e4dd1f": [ 198 | 3 199 | ], 200 | "d6d9128e": [ 201 | 3 202 | ], 203 | "521f4902": [ 204 | 3 205 | ], 206 | "8618bf6e": [ 207 | 3 208 | ], 209 | "f9ce2dae": [ 210 | 3 211 | ], 212 | "2f06d400": [ 213 | 3 214 | ], 215 | "5b7b1476": [ 216 | 4, 217 | 6 218 | ], 219 | "026a70b9": [ 220 | 4, 221 | 6 222 | ], 223 | "02ec155f": [ 224 | 4, 225 | 6 226 | ], 227 | "6e17334a": [ 228 | 3 229 | ], 230 | "20b0aae6": [ 231 | 3 232 | ], 233 | "4422b14f": [ 234 | 3 235 | ], 236 | "42da7efd": [ 237 | 4, 238 | 6 239 | ], 240 | "2310b18a": [ 241 | 3 242 | ], 243 | "2c02bf74": [ 244 | 3 245 | ], 246 | "8084914e": [ 247 | 4, 248 | 6 249 | ], 250 | "f764188e": [ 251 | 3 252 | ], 253 | "1cdd45e8": [ 254 | 3 255 | ], 256 | "7fb78e35": [ 257 | 3 258 | ], 259 | "0b2ae428": [ 260 | 3 261 | ], 262 | "2961e65b": [ 263 | 3 264 | ], 265 | "55ad5e21": [ 266 | 3 267 | ], 268 | "588e4fe2": [ 269 | 3 270 | ], 271 | "30e4adc4": [ 272 | 3 273 | ], 274 | "09ee3910": [ 275 | 3 276 | ], 277 | "18f1c490": [ 278 | 3 279 | ], 280 | "07d3c044": [ 281 | 3 282 | ], 283 | "8d2828ba": [ 284 | 3 285 | ], 286 | "97cf2e44": [ 287 | 3 288 | ], 289 | "2eca343e": [ 290 | 3 291 | ], 292 | "b96ac82c": [ 293 | 3 294 | ], 295 | "32a8aaf2": [ 296 | 3 297 | ], 298 | "fd710bda": [ 299 | 3 300 | ], 301 | "5c9ff047": [ 302 | 3 303 | ], 304 | "b6d71d58": [ 305 | 3 306 | ], 307 | "ed0ee3d8": [ 308 | 3 309 | ], 310 | "0435b99e": [ 311 | 3 312 | ], 313 | "28a38085": [ 314 | 3 315 | ], 316 | "0e039a2a": [ 317 | 3 318 | ], 319 | "6a207438": [ 320 | 3 321 | ], 322 | "5c6e4e1b": [ 323 | 3 324 | ], 325 | "b0b463fe": [ 326 | 3 327 | ], 328 | "584a70cc": [ 329 | 3 330 | ], 331 | "6354dc2a": [ 332 | 3 333 | ], 334 | "29574f8b": [ 335 | 3 336 | ], 337 | "347b1654": [ 338 | 3 339 | ], 340 | "5f2dfc60": [ 341 | 3 342 | ], 343 | "5ac3a5d2": [ 344 | 3 345 | ], 346 | "1a3d78bd": [ 347 | 3 348 | ], 349 | "200679f4": [ 350 | 3 351 | ], 352 | "370e3178": [ 353 | 3 354 | ], 355 | "6686e9a6": [ 356 | 3 357 | ], 358 | "57eb1f0a": [ 359 | 3 360 | ], 361 | "6ac500ce": [ 362 | 3 363 | ], 364 | "0ef44fce": [ 365 | 3 366 | ], 367 | "466de31a": [ 368 | 3 369 | ], 370 | "7d67231c": [ 371 | 3 372 | ], 373 | "27528dda": [ 374 | 3 375 | ], 376 | "263a3e50": [ 377 | 3 378 | ], 379 | "8b63dd56": [ 380 | 3 381 | ], 382 | "7570d163": [ 383 | 3 384 | ], 385 | "c1955f62": [ 386 | 3 387 | ], 388 | "6f8d35d8": [ 389 | 3 390 | ], 391 | "0e857228": [ 392 | 3 393 | ], 394 | "5b7fb14a": [ 395 | 3 396 | ], 397 | "9c5e9ba4": [ 398 | 3 399 | ], 400 | "c4345e5a": [ 401 | 4, 402 | 6 403 | ], 404 | "6a384e62": [ 405 | 3 406 | ], 407 | "231cbf2a": [ 408 | 3 409 | ], 410 | "2c30cb6a": [ 411 | 1 412 | ], 413 | "5b4045b8": [ 414 | 1 415 | ], 416 | "083adcf4": [ 417 | 1 418 | ], 419 | "a27b3e10": [ 420 | 1 421 | ], 422 | "c425ac3a": [ 423 | 0 424 | ], 425 | "6606b79e": [ 426 | 0 427 | ], 428 | "4fb61960": [ 429 | 0 430 | ], 431 | "1ac3bcae": [ 432 | 0 433 | ], 434 | "0bdd6e24": [ 435 | 0 436 | ], 437 | "5229dd1c": [ 438 | 0 439 | ], 440 | "62944f18": [ 441 | 2 442 | ], 443 | "dcb86d34": [ 444 | 2 445 | ], 446 | "06fec79e": [ 447 | 1 448 | ], 449 | "fcaed122": [ 450 | 0 451 | ], 452 | "e1717f6e": [ 453 | 2 454 | ] 455 | } 456 | } -------------------------------------------------------------------------------- /dist/vue-ssr-server-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "entry": "server-bundle.js", 3 | "files": { 4 | "0.server-bundle.js": "exports.ids=[0],exports.modules={19:function(e,t,s){var n=s(20);\"string\"==typeof n&&(n=[[e.i,n,\"\"]]),n.locals&&(e.exports=n.locals);var i=s(14);e.exports.__inject__=function(e){i(\"0b48fd67\",n,!0,e)}},20:function(e,t,s){t=e.exports=s(13)(void 0),t.push([e.i,\".item-view-header{background-color:#fff;padding:1.8em 2em 1em;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1);box-shadow:0 1px 2px rgba(0,0,0,.1)}.item-view-header h1{display:inline;font-size:1.5em;margin:0;margin-right:.5em}.item-view-header .host,.item-view-header .meta,.item-view-header .meta a{color:#828282}.item-view-header .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}.item-view-comments-header .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}}\",\"\"])},21:function(e,t,s){var n=s(22);\"string\"==typeof n&&(n=[[e.i,n,\"\"]]),n.locals&&(e.exports=n.locals);var i=s(14);e.exports.__inject__=function(e){i(\"aa38e5f4\",n,!0,e)}},22:function(e,t,s){t=e.exports=s(13)(void 0),t.push([e.i,\".spinner{-webkit-transition:opacity .15s ease;-o-transition:opacity .15s ease;transition:opacity .15s ease;-webkit-animation:rotator 1.4s linear infinite;animation:rotator 1.4s linear infinite;-webkit-animation-play-state:paused;animation-play-state:paused}.spinner.show{-webkit-animation-play-state:running;animation-play-state:running}.spinner.v-enter,.spinner.v-leave-active{opacity:0}.spinner.v-enter-active,.spinner.v-leave{opacity:1}.spinner .path{stroke:#f60;stroke-dasharray:126;stroke-dashoffset:0;-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-animation:dash 1.4s ease-in-out infinite;animation:dash 1.4s ease-in-out infinite}@-webkit-keyframes rotator{0%{-webkit-transform:scale(.5) rotate(0deg);transform:scale(.5) rotate(0deg)}to{-webkit-transform:scale(.5) rotate(270deg);transform:scale(.5) rotate(270deg)}}@keyframes rotator{0%{-webkit-transform:scale(.5) rotate(0deg);transform:scale(.5) rotate(0deg)}to{-webkit-transform:scale(.5) rotate(270deg);transform:scale(.5) rotate(270deg)}}@-webkit-keyframes dash{0%{stroke-dashoffset:126}50%{stroke-dashoffset:63;-webkit-transform:rotate(135deg);transform:rotate(135deg)}to{stroke-dashoffset:126;-webkit-transform:rotate(450deg);transform:rotate(450deg)}}@keyframes dash{0%{stroke-dashoffset:126}50%{stroke-dashoffset:63;-webkit-transform:rotate(135deg);transform:rotate(135deg)}to{stroke-dashoffset:126;-webkit-transform:rotate(450deg);transform:rotate(450deg)}}\",\"\"])},23:function(e,t,s){var n=s(24);\"string\"==typeof n&&(n=[[e.i,n,\"\"]]),n.locals&&(e.exports=n.locals);var i=s(14);e.exports.__inject__=function(e){i(\"c2a61e7c\",n,!0,e)}},24:function(e,t,s){t=e.exports=s(13)(void 0),t.push([e.i,\".comment-children .comment-children{margin-left:1.5em}.comment{border-top:1px solid #eee;position:relative}.comment .by,.comment .text,.comment .toggle{font-size:.9em;margin:1em 0}.comment .by{color:#828282}.comment .by a{color:#828282;text-decoration:underline}.comment .text{overflow-wrap:break-word}.comment .text a:hover{color:#f60}.comment .text pre{white-space:pre-wrap}.comment .toggle{background-color:#fffbf2;padding:.3em .5em;border-radius:4px}.comment .toggle a{color:#828282;cursor:pointer}.comment .toggle.open{padding:0;background-color:transparent;margin-bottom:-.5em}\",\"\"])},28:function(e,t,s){\"use strict\";function n(e){var t;t=s(21),t.__inject__&&t.__inject__(e)}function i(e){var t;t=s(23),t.__inject__&&t.__inject__(e)}function o(e,t){if(t&&t.kids)return e.dispatch(\"FETCH_ITEMS\",{ids:t.kids}).then(function(){return Promise.all(t.kids.map(function(t){return o(e,e.state.items[t])}))})}function r(e){var t;t=s(19),t.__inject__&&t.__inject__(e)}Object.defineProperty(t,\"__esModule\",{value:!0});var a={name:\"spinner\",props:[\"show\"],serverCacheKey:function(e){return e.show}},m=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s(\"transition\",[s(\"svg\",{directives:[{name:\"show\",rawName:\"v-show\",value:e.show,expression:\"show\"}],staticClass:\"spinner\",class:{show:e.show},attrs:{width:\"44px\",height:\"44px\",viewBox:\"0 0 44 44\"}},[s(\"circle\",{staticClass:\"path\",attrs:{fill:\"none\",\"stroke-width\":\"4\",\"stroke-linecap\":\"round\",cx:\"22\",cy:\"22\",r:\"20\"}})])])},c=[],d={render:m,staticRenderFns:c},l=d,p=s(1),f=n,u=p(a,l,!1,f,null,\"75d339dc\"),_=u.exports,h={name:\"comment\",props:[\"id\"],data:function(){return{open:!0}},computed:{comment:function(){return this.$store.state.items[this.id]}},methods:{pluralize:function(e){return e+(1===e?\" reply\":\" replies\")}}},g=function(){var e=this,t=e.$createElement,s=e._self._c||t;return e.comment?s(\"li\",{staticClass:\"comment\"},[e._ssrNode('
',\"
\",[s(\"router-link\",{attrs:{to:\"/user/\"+e.comment.by}},[e._v(e._s(e.comment.by))]),e._ssrNode(e._ssrEscape(\"\\n \"+e._s(e._f(\"timeAgo\")(e.comment.time))+\" ago\\n \"))],2),e._ssrNode('
'+e._s(e.comment.text)+\"
\"+(e.comment.kids&&e.comment.kids.length?\"\"+e._ssrEscape(e._s(e.open?\"[-]\":\"[+] \"+e.pluralize(e.comment.kids.length)+\" collapsed\"))+\"\":\"\\x3c!----\\x3e\")),e._ssrNode('
    \",\"
\",e._l(e.comment.kids,function(e){return s(\"comment\",{key:e,attrs:{id:e}})}))],2):e._e()},v=[],w={render:g,staticRenderFns:v},k=w,b=s(1),x=i,y=b(h,k,!1,x,null,\"4fcee322\"),N=y.exports,E={name:\"item-view\",components:{Spinner:_,Comment:N},data:function(){return{loading:!0}},computed:{item:function(){return this.$store.state.items[this.$route.params.id]}},asyncData:function(e){var t=e.store,s=e.route.params.id;return t.dispatch(\"FETCH_ITEMS\",{ids:[s]})},title:function(){return this.item.title},beforeMount:function(){this.fetchComments()},watch:{item:\"fetchComments\"},methods:{fetchComments:function(){var e=this;this.item&&this.item.kids&&(this.loading=!0,o(this.$store,this.item).then(function(){e.loading=!1}))}}},C=function(){var e=this,t=e.$createElement,s=e._self._c||t;return e.item?s(\"div\",{staticClass:\"item-view\"},[e.item?[e._ssrNode('
',\"
\",[e._ssrNode(\"

'+e._ssrEscape(e._s(e.item.title))+\"

\"+(e.item.url?''+e._ssrEscape(\"\\n (\"+e._s(e._f(\"host\")(e.item.url))+\")\\n \")+\"\":\"\\x3c!----\\x3e\")),e._ssrNode('

',\"

\",[e._ssrNode(e._ssrEscape(\"\\n \"+e._s(e.item.score)+\" points\\n | by \")),s(\"router-link\",{attrs:{to:\"/user/\"+e.item.by}},[e._v(e._s(e.item.by))]),e._ssrNode(e._ssrEscape(\"\\n \"+e._s(e._f(\"timeAgo\")(e.item.time))+\" ago\\n \"))],2)],2),e._ssrNode('
',\"
\",[e._ssrNode('

',\"

\",[e._ssrNode(e._ssrEscape(\"\\n \"+e._s(e.item.kids?e.item.descendants+\" comments\":\"No comments yet.\")+\"\\n \")),s(\"spinner\",{attrs:{show:e.loading}})],2),e.loading?e._e():e._ssrNode('
    ',\"
\",e._l(e.item.kids,function(e){return s(\"comment\",{key:e,attrs:{id:e}})}))])]:e._e()],2):e._e()},j=[],$={render:C,staticRenderFns:j},z=$,F=s(1),M=r,S=F(E,z,!1,M,null,\"5fe69b56\");t.default=S.exports}};", 5 | "1.server-bundle.js": "exports.ids=[1],exports.modules={15:function(t,e,s){var i=s(16);\"string\"==typeof i&&(i=[[t.i,i,\"\"]]),i.locals&&(t.exports=i.locals);var a=s(14);t.exports.__inject__=function(t){a(\"43656ec8\",i,!0,t)}},16:function(t,e,s){e=t.exports=s(13)(void 0),e.push([t.i,\".news-view{padding-top:45px}.news-list,.news-list-nav{background-color:#fff;border-radius:2px}.news-list-nav{padding:15px 30px;position:fixed;text-align:center;top:55px;left:0;right:0;z-index:998;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.1);box-shadow:0 1px 2px rgba(0,0,0,.1)}.news-list-nav a{margin:0 1em}.news-list-nav .disabled{color:#ccc}.news-list{position:absolute;margin:30px 0;width:100%;-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);-o-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1)}.news-list ul{list-style-type:none;padding:0;margin:0}.slide-left-enter,.slide-right-leave-to{opacity:0;-webkit-transform:translate(30px);-ms-transform:translate(30px);transform:translate(30px)}.slide-left-leave-to,.slide-right-enter{opacity:0;-webkit-transform:translate(-30px);-ms-transform:translate(-30px);transform:translate(-30px)}.item-enter-active,.item-leave-active,.item-move{-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);-o-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1)}.item-enter,.item-leave-active{opacity:0;-webkit-transform:translate(30px);-ms-transform:translate(30px);transform:translate(30px)}.item-leave-active{position:absolute}@media (max-width:600px){.news-list{margin:10px 0}}\",\"\"])},17:function(t,e,s){var i=s(18);\"string\"==typeof i&&(i=[[t.i,i,\"\"]]),i.locals&&(t.exports=i.locals);var a=s(14);t.exports.__inject__=function(t){a(\"70084b98\",i,!0,t)}},18:function(t,e,s){e=t.exports=s(13)(void 0),e.push([t.i,\".news-item{background-color:#fff;padding:20px 30px 20px 80px;border-bottom:1px solid #eee;position:relative;line-height:20px}.news-item .score{color:#f60;font-size:1.1em;font-weight:700;position:absolute;top:50%;left:0;width:80px;text-align:center;margin-top:-10px}.news-item .host,.news-item .meta{font-size:.85em;color:#828282}.news-item .host a,.news-item .meta a{color:#828282;text-decoration:underline}.news-item .host a:hover,.news-item .meta a:hover{color:#f60}\",\"\"])},27:function(t,e,s){\"use strict\";function i(t){var e;e=s(17),e.__inject__&&e.__inject__(t)}function a(t){var e;e=s(15),e.__inject__&&e.__inject__(t)}function n(t){return{name:t+\"-stories-view\",asyncData:function(e){return e.store.dispatch(\"FETCH_LIST_DATA\",{type:t})},title:E(t),render:function(e){return e(N,{props:{type:t}})}}}Object.defineProperty(e,\"__esModule\",{value:!0});var r=s(2),o=s(3),l={name:\"news-item\",props:[\"item\"],serverCacheKey:function(t){var e=t.item,s=e.id,i=e.__lastUpdated,a=e.time;return s+\"::\"+i+\"::\"+Object(o.timeAgo)(a)}},p=function(){var t=this,e=t.$createElement,s=t._self._c||e;return s(\"li\",{staticClass:\"news-item\"},[t._ssrNode(''+t._ssrEscape(t._s(t.item.score))+\"\"),t._ssrNode('',\"\",[t.item.url?[t._ssrNode(\"'+t._ssrEscape(t._s(t.item.title))+''+t._ssrEscape(\" (\"+t._s(t._f(\"host\")(t.item.url))+\")\")+\"\")]:[s(\"router-link\",{attrs:{to:\"/item/\"+t.item.id}},[t._v(t._s(t.item.title))])]],2),t._ssrNode(\"
\"),t._ssrNode('',\"\",[\"job\"!==t.item.type?t._ssrNode('',\"\",[t._ssrNode(\"\\n by \"),s(\"router-link\",{attrs:{to:\"/user/\"+t.item.by}},[t._v(t._s(t.item.by))])],2):t._e(),t._ssrNode(''+t._ssrEscape(\"\\n \"+t._s(t._f(\"timeAgo\")(t.item.time))+\" ago\\n \")+\"\"),\"job\"!==t.item.type?t._ssrNode('',\"\",[t._ssrNode(\"\\n | \"),s(\"router-link\",{attrs:{to:\"/item/\"+t.item.id}},[t._v(t._s(t.item.descendants)+\" comments\")])],2):t._e()],2),t._ssrNode(\"story\"!==t.item.type?''+t._ssrEscape(t._s(t.item.type))+\"\":\"\\x3c!----\\x3e\")],2)},c=[],m={render:p,staticRenderFns:c},d=m,u=s(1),_=i,f=u(l,d,!1,_,null,\"4ec58b40\"),h=f.exports,v={name:\"item-list\",components:{Item:h},props:{type:String},data:function(){return{transition:\"slide-right\",displayedPage:Number(this.$route.params.page)||1,displayedItems:this.$store.getters.activeItems}},computed:{page:function(){return Number(this.$route.params.page)||1},maxPage:function(){var t=this.$store.state,e=t.itemsPerPage,s=t.lists;return Math.ceil(s[this.type].length/e)},hasMore:function(){return this.pages.maxPage)return void s.$router.replace(\"/\"+s.type+\"/1\");s.transition=-1===e?null:t>e?\"slide-left\":\"slide-right\",s.displayedPage=t,s.displayedItems=s.$store.getters.activeItems,s.$bar.finish()})}}},g=function(){var t=this,e=t.$createElement,s=t._self._c||e;return s(\"div\",{staticClass:\"news-view\"},[t._ssrNode('
',\"
\",[t.page>1?s(\"router-link\",{attrs:{to:\"/\"+t.type+\"/\"+(t.page-1)}},[t._v(\"< prev\")]):s(\"a\",{staticClass:\"disabled\"},[t._v(\"< prev\")]),t._ssrNode(\"\"+t._ssrEscape(t._s(t.page)+\"/\"+t._s(t.maxPage))+\"\"),t.hasMore?s(\"router-link\",{attrs:{to:\"/\"+t.type+\"/\"+(t.page+1)}},[t._v(\"more >\")]):s(\"a\",{staticClass:\"disabled\"},[t._v(\"more >\")])],2),s(\"transition\",{attrs:{name:t.transition}},[t.displayedPage>0?s(\"div\",{key:t.displayedPage,staticClass:\"news-list\"},[s(\"transition-group\",{attrs:{tag:\"ul\",name:\"item\"}},t._l(t.displayedItems,function(t){return s(\"item\",{key:t.id,attrs:{item:t}})}))],1):t._e()])],1)},b=[],x={render:g,staticRenderFns:b},y=x,w=s(1),k=a,I=w(v,y,!1,k,null,\"4e0af864\"),N=I.exports;e.default=n;var E=function(t){return t.charAt(0).toUpperCase()+t.slice(1)}}};", 6 | "2.server-bundle.js": "exports.ids=[2],exports.modules={25:function(e,s,r){var t=r(26);\"string\"==typeof t&&(t=[[e.i,t,\"\"]]),t.locals&&(e.exports=t.locals);var i=r(14);e.exports.__inject__=function(e){i(\"45ff866c\",t,!0,e)}},26:function(e,s,r){s=e.exports=r(13)(void 0),s.push([e.i,\".user-view{background-color:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2em 3em}.user-view h1{margin:0;font-size:1.5em}.user-view .meta{list-style-type:none;padding:0}.user-view .label{display:inline-block;min-width:4em}.user-view .about{margin:1em 0}.user-view .links a{text-decoration:underline}\",\"\"])},29:function(e,s,r){\"use strict\";function t(e){var s;s=r(25),s.__inject__&&s.__inject__(e)}Object.defineProperty(s,\"__esModule\",{value:!0});var i={name:\"user-view\",computed:{user:function(){return this.$store.state.users[this.$route.params.id]}},asyncData:function(e){var s=e.store,r=e.route.params.id;return s.dispatch(\"FETCH_USER\",{id:r})},title:function(){return this.user?this.user.id:\"User not found\"}},a=function(){var e=this,s=e.$createElement;return(e._self._c||s)(\"div\",{staticClass:\"user-view\"},[e._ssrNode(e.user?\"

\"+e._ssrEscape(\"User : \"+e._s(e.user.id))+'

  • Created:'+e._ssrEscape(\" \"+e._s(e._f(\"timeAgo\")(e.user.created))+\" ago\")+'
  • Karma:'+e._ssrEscape(\" \"+e._s(e.user.karma))+\"
  • \"+(e.user.about?'
  • '+e._s(e.user.about)+\"
  • \":\"\\x3c!----\\x3e\")+'

submissions |\\n comments

\":!1===e.user?\"

User not found.

\":\"\\x3c!----\\x3e\")])},n=[],o={render:a,staticRenderFns:n},u=o,c=r(1),l=t,d=c(i,u,!1,l,null,\"7cd1eb0d\");s.default=d.exports}};", 7 | "server-bundle.js": "module.exports=function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={},r={3:0};return t.e=function(t){if(0!==r[t]){var n=require(\"./\"+t+\".server-bundle.js\"),o=n.modules,i=n.ids;for(var s in o)e[s]=o[s];for(var a=0;a',\"\",[e._ssrNode('\",[n(\"router-link\",{attrs:{to:\"/\",exact:\"\"}},[n(\"img\",{staticClass:\"logo\",attrs:{src:\"/public/logo-48.png\",alt:\"logo\"}})]),n(\"router-link\",{attrs:{to:\"/top\"}},[e._v(\"Top\")]),n(\"router-link\",{attrs:{to:\"/new\"}},[e._v(\"New\")]),n(\"router-link\",{attrs:{to:\"/show\"}},[e._v(\"Show\")]),n(\"router-link\",{attrs:{to:\"/ask\"}},[e._v(\"Ask\")]),n(\"router-link\",{attrs:{to:\"/job\"}},[e._v(\"Jobs\")]),e._ssrNode('\\n Built with Vue.js\\n ')],2)]),n(\"transition\",{attrs:{name:\"fade\",mode:\"out-in\"}},[n(\"router-view\",{staticClass:\"view\"})],1)],1)},d=[],l={render:f,staticRenderFns:d},p=l,v=n(1),h=r,_=v(null,p,!1,h,null,\"2d903955\"),m=_.exports,g=n(7),b=n.n(g),E=n(2),y={FETCH_LIST_DATA:function(e,t){var n=e.commit,r=e.dispatch,o=(e.state,t.type);return n(\"SET_ACTIVE_TYPE\",{type:o}),Object(E.a)(o).then(function(e){return n(\"SET_LIST\",{type:o,ids:e})}).then(function(){return r(\"ENSURE_ACTIVE_ITEMS\")})},ENSURE_ACTIVE_ITEMS:function(e){return(0,e.dispatch)(\"FETCH_ITEMS\",{ids:e.getters.activeIds})},FETCH_ITEMS:function(e,t){var n=e.commit,r=e.state,o=t.ids,i=Date.now();return o=o.filter(function(e){var t=r.items[e];return!t||i-t.__lastUpdated>18e4}),o.length?Object(E.b)(o).then(function(e){return n(\"SET_ITEMS\",{items:e})}):Promise.resolve()},FETCH_USER:function(e,t){var n=e.commit,r=e.state,o=t.id;return r.users[o]?Promise.resolve(r.users[o]):Object(E.c)(o).then(function(e){return n(\"SET_USER\",{id:o,user:e})})}},T={SET_ACTIVE_TYPE:function(e,t){var n=t.type;e.activeType=n},SET_LIST:function(e,t){var n=t.type,r=t.ids;e.lists[n]=r},SET_ITEMS:function(e,t){t.items.forEach(function(t){t&&c.a.set(e.items,t.id,t)})},SET_USER:function(e,t){var n=t.id,r=t.user;c.a.set(e.users,n,r||!1)}},S={activeIds:function(e){var t=e.activeType,n=e.itemsPerPage,r=e.lists;if(!t)return[];var o=Number(e.route.params.page)||1,i=(o-1)*n,s=o*n;return r[t].slice(i,s)},activeItems:function(e,t){return t.activeIds.map(function(t){return e.items[t]}).filter(function(e){return e})}};c.a.use(b.a);var w=n(11),x=n.n(w);c.a.use(x.a);var I=function(e){return function(){return n.e(1).then(n.bind(null,27)).then(function(t){return t.default(e)})}},j=function(){return n.e(0).then(n.bind(null,28))},P=function(){return n.e(2).then(n.bind(null,29))},C=n(12),R={created:function(){var e=s(this);e&&(this.$ssrContext.title=\"Vue HN 2.0 | \"+e)}},k=R,O=n(3);c.a.mixin(k),Object.keys(O).forEach(function(e){c.a.filter(e,O[e])});t.default=function(e){return new Promise(function(t,n){var r=a(),o=r.app,i=r.router,s=r.store,u=e.url,c=i.resolve(u).route.fullPath;if(c!==u)return n({url:c});i.push(u),i.onReady(function(){var r=i.getMatchedComponents();if(!r.length)return n({code:404});Promise.all(r.map(function(e){var t=e.asyncData;return t&&t({store:s,route:i.currentRoute})})).then(function(){e.state=s.state,t(o)}).catch(n)},n)})}},function(e,t){},function(e,t){e.exports=function(e,t){for(var n=[],r={},o=0;o\"+r.css+\"\"}return t}var s=n(6);e.exports=function(e,t,n,a){if(a||\"undefined\"==typeof __VUE_SSR_CONTEXT__||(a=__VUE_SSR_CONTEXT__),a){a.hasOwnProperty(\"styles\")||(Object.defineProperty(a,\"styles\",{enumerable:!0,get:function(){return i(a._styles)}}),a._renderStyles=i);var u=a._styles||(a._styles={});t=s(e,t),n?r(u,t):o(u,t)}}}]);" 8 | }, 9 | "maps": {} 10 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue Hackernews 2.0", 3 | "short_name": "Vue HN", 4 | "icons": [{ 5 | "src": "/public/logo-120.png", 6 | "sizes": "120x120", 7 | "type": "image/png" 8 | }, { 9 | "src": "/public/logo-144.png", 10 | "sizes": "144x144", 11 | "type": "image/png" 12 | }, { 13 | "src": "/public/logo-152.png", 14 | "sizes": "152x152", 15 | "type": "image/png" 16 | }, { 17 | "src": "/public/logo-192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, { 21 | "src": "/public/logo-256.png", 22 | "sizes": "256x256", 23 | "type": "image/png" 24 | }, { 25 | "src": "/public/logo-384.png", 26 | "sizes": "384x384", 27 | "type": "image/png" 28 | }, { 29 | "src": "/public/logo-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png" 32 | }], 33 | "start_url": "/", 34 | "background_color": "#f2f3f5", 35 | "display": "standalone", 36 | "theme_color": "#f60" 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hackernews-2.0-typescript", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "kevguy ", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "node server", 9 | "start": "cross-env NODE_ENV=production node server", 10 | "build": "rimraf dist && npm run build:client && npm run build:server", 11 | "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress --hide-modules", 12 | "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress --hide-modules" 13 | }, 14 | "dependencies": { 15 | "compression": "^1.7.1", 16 | "cross-env": "^5.1.1", 17 | "es6-promise": "^4.1.1", 18 | "express": "^4.16.2", 19 | "extract-text-webpack-plugin": "^3.0.2", 20 | "firebase": "4.6.2", 21 | "lru-cache": "^4.1.1", 22 | "route-cache": "0.4.3", 23 | "serve-favicon": "^2.4.5", 24 | "vue": "^2.5.3", 25 | "vue-router": "^3.0.1", 26 | "vue-server-renderer": "^2.5.3", 27 | "vuex": "^3.0.1", 28 | "vuex-router-sync": "^5.0.0" 29 | }, 30 | "devDependencies": { 31 | "@types/es6-promise": "^0.0.33", 32 | "@types/lru-cache": "^4.1.0", 33 | "autoprefixer": "^7.1.6", 34 | "babel-core": "^6.26.0", 35 | "babel-loader": "^7.1.2", 36 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 37 | "babel-preset-env": "^1.6.1", 38 | "chokidar": "^1.7.0", 39 | "css-loader": "^0.28.7", 40 | "file-loader": "^1.1.5", 41 | "friendly-errors-webpack-plugin": "^1.6.1", 42 | "rimraf": "^2.6.2", 43 | "stylus": "^0.54.5", 44 | "stylus-loader": "^3.0.1", 45 | "sw-precache-webpack-plugin": "^0.11.4", 46 | "ts-loader": "^3.2.0", 47 | "typescript": "^2.6.2", 48 | "url-loader": "^0.6.2", 49 | "vue-loader": "^13.5.0", 50 | "vue-style-loader": "^3.0.3", 51 | "vue-template-compiler": "^2.5.3", 52 | "webpack": "^3.8.1", 53 | "webpack-dev-middleware": "^1.12.0", 54 | "webpack-hot-middleware": "^2.20.0", 55 | "webpack-merge": "^4.1.1", 56 | "webpack-node-externals": "^1.6.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/logo-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-120.png -------------------------------------------------------------------------------- /public/logo-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-144.png -------------------------------------------------------------------------------- /public/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-152.png -------------------------------------------------------------------------------- /public/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-192.png -------------------------------------------------------------------------------- /public/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-256.png -------------------------------------------------------------------------------- /public/logo-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-384.png -------------------------------------------------------------------------------- /public/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-48.png -------------------------------------------------------------------------------- /public/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/public/logo-512.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const LRU = require('lru-cache') 4 | const express = require('express') 5 | const favicon = require('serve-favicon') 6 | const compression = require('compression') 7 | const microcache = require('route-cache') 8 | const resolve = file => path.resolve(__dirname, file) 9 | const { createBundleRenderer } = require('vue-server-renderer') 10 | 11 | const isProd = process.env.NODE_ENV === 'production' 12 | const useMicroCache = process.env.MICRO_CACHE !== 'false' 13 | const serverInfo = 14 | `express/${require('express/package.json').version} ` + 15 | `vue-server-renderer/${require('vue-server-renderer/package.json').version}` 16 | 17 | const app = express() 18 | 19 | function createRenderer (bundle, options) { 20 | // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer 21 | return createBundleRenderer(bundle, Object.assign(options, { 22 | // for component caching 23 | cache: LRU({ 24 | max: 1000, 25 | maxAge: 1000 * 60 * 15 26 | }), 27 | // this is only needed when vue-server-renderer is npm-linked 28 | basedir: resolve('./dist'), 29 | // recommended for performance 30 | runInNewContext: false 31 | })) 32 | } 33 | 34 | let renderer 35 | let readyPromise 36 | const templatePath = resolve('./src/index.template.html') 37 | if (isProd) { 38 | // In production: create server renderer using template and built server bundle. 39 | // The server bundle is generated by vue-ssr-webpack-plugin. 40 | const template = fs.readFileSync(templatePath, 'utf-8') 41 | const bundle = require('./dist/vue-ssr-server-bundle.json') 42 | // The client manifests are optional, but it allows the renderer 43 | // to automatically infer preload/prefetch links and directly add 40 | 41 | 75 | -------------------------------------------------------------------------------- /src/components/Item.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 46 | 47 | 73 | -------------------------------------------------------------------------------- /src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 88 | 89 | 103 | -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 55 | -------------------------------------------------------------------------------- /src/entry-client.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import 'es6-promise/auto'; 3 | import { createApp } from './app'; 4 | import ProgressBar from './components/ProgressBar.vue'; 5 | 6 | // global progress bar 7 | const bar: any = Vue.prototype.$bar = new Vue(ProgressBar).$mount(); 8 | document.body.appendChild(bar.$el); 9 | 10 | // a global mixin that calls `asyncData` when a route component's params change 11 | Vue.mixin({ 12 | beforeRouteUpdate (to: any, from: any, next: any) { 13 | const { asyncData } = (this as any).$options; 14 | if (asyncData) { 15 | asyncData({ 16 | store: (this as any).$store, 17 | route: to 18 | }).then(next).catch(next); 19 | } else { 20 | next(); 21 | } 22 | } 23 | }) 24 | 25 | const { app, router, store }: any = createApp(); 26 | 27 | // prime the store with server-initialized state. 28 | // the state is determined during SSR and inlined in the page markup. 29 | if (window.__INITIAL_STATE__) { 30 | store.replaceState(window.__INITIAL_STATE__); 31 | } 32 | 33 | // wait until router has resolved all async before hooks 34 | // and async components... 35 | router.onReady(() => { 36 | // Add router hook for handling asyncData. 37 | // Doing it after initial route is resolved so that we don't double-fetch 38 | // the data that we already have. Using router.beforeResolve() so that all 39 | // async components are resolved. 40 | router.beforeResolve((to: any, from: any, next: any) => { 41 | const matched = router.getMatchedComponents(to); 42 | const prevMatched = router.getMatchedComponents(from); 43 | let diffed = false; 44 | const activated = matched.filter((c: any, i: any) => { 45 | return diffed || (diffed = (prevMatched[i] !== c)); 46 | }) 47 | const asyncDataHooks: any = activated.map((c: any) => c.asyncData).filter((_: any) => _); 48 | if (!asyncDataHooks.length) { 49 | return next(); 50 | } 51 | 52 | bar.start(); 53 | Promise.all(asyncDataHooks.map((hook: any) => hook({ store, route: to }))) 54 | .then(() => { 55 | bar.finish(); 56 | next(); 57 | }) 58 | .catch(next); 59 | }); 60 | 61 | // actually mount to DOM 62 | app.$mount('#app'); 63 | }); 64 | 65 | // service worker 66 | if ('https:' === location.protocol && navigator.serviceWorker) { 67 | navigator.serviceWorker.register('/service-worker.js'); 68 | } 69 | -------------------------------------------------------------------------------- /src/entry-server.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from './app' 2 | 3 | const isDev = process.env.NODE_ENV !== 'production' 4 | 5 | // This exported function will be called by `bundleRenderer`. 6 | // This is where we perform data-prefetching to determine the 7 | // state of our application before actually rendering it. 8 | // Since data fetching is async, this function is expected to 9 | // return a Promise that resolves to the app instance. 10 | export default (context: any) => { 11 | return new Promise((resolve, reject) => { 12 | const s: any = isDev && Date.now() 13 | const { app, router, store } = createApp() 14 | 15 | const { url } = context 16 | const { fullPath } = router.resolve(url).route 17 | 18 | if (fullPath !== url) { 19 | return reject({ url: fullPath }) 20 | } 21 | 22 | // set router's location 23 | router.push(url) 24 | 25 | // wait until router has resolved possible async hooks 26 | router.onReady(() => { 27 | const matchedComponents = router.getMatchedComponents() 28 | // no matched routes 29 | if (!matchedComponents.length) { 30 | return reject({ code: 404 }) 31 | } 32 | // Call fetchData hooks on components matched by the route. 33 | // A preFetch hook dispatches a store action and returns a Promise, 34 | // which is resolved when the action is complete and store state has been 35 | // updated. 36 | Promise.all(matchedComponents.map(({ asyncData }: any) => asyncData && asyncData({ 37 | store, 38 | route: router.currentRoute 39 | }))).then(() => { 40 | isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) 41 | // After all preFetch hooks are resolved, our store is now 42 | // filled with the state needed to render the app. 43 | // Expose the state on the render context, and let the request handler 44 | // inline the state in the HTML response. This allows the client-side 45 | // store to pick-up the server-side state without having to duplicate 46 | // the initial data fetching on the client. 47 | context.state = store.state 48 | resolve(app) 49 | }).catch(reject) 50 | }, reject) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/public/logo-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-120.png -------------------------------------------------------------------------------- /src/public/logo-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-144.png -------------------------------------------------------------------------------- /src/public/logo-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-152.png -------------------------------------------------------------------------------- /src/public/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-192.png -------------------------------------------------------------------------------- /src/public/logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-256.png -------------------------------------------------------------------------------- /src/public/logo-384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-384.png -------------------------------------------------------------------------------- /src/public/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-48.png -------------------------------------------------------------------------------- /src/public/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevguy/vue-hackernews-2.0-typescript/bf8ac9bcb33ecc4c06f7c995f0c030fee82b47bf/src/public/logo-512.png -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router, { RouterOptions } from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | // route-level code splitting 7 | const createListView = (id: string) => () => import('../views/CreateListView').then((m: any) => m.default(id)) 8 | const ItemView = () => import('../views/ItemView.vue') 9 | const UserView = () => import('../views/UserView.vue') 10 | 11 | export function createRouter (): Router { 12 | return new Router({ 13 | mode: 'history', 14 | fallback: false, 15 | scrollBehavior: () => ({ x: 0, y: 0 }), 16 | routes: [ 17 | { path: '/top/:page(\\d+)?', component: createListView('top') }, 18 | { path: '/new/:page(\\d+)?', component: createListView('new') }, 19 | { path: '/show/:page(\\d+)?', component: createListView('show') }, 20 | { path: '/ask/:page(\\d+)?', component: createListView('ask') }, 21 | { path: '/job/:page(\\d+)?', component: createListView('job') }, 22 | { path: '/item/:id(\\d+)', component: ItemView }, 23 | { path: '/user/:id', component: UserView }, 24 | { path: '/', redirect: '/top' } 25 | ] 26 | } as RouterOptions) 27 | } 28 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | fetchUser, 3 | fetchItems, 4 | fetchIdsByType 5 | } from '../api' 6 | 7 | import { State } from './index'; 8 | import { ActionTree, ActionContext, Dispatch, Commit } from "vuex"; 9 | 10 | // type Partial = { 11 | // [P in keyof T]?: T[P]; 12 | // } 13 | 14 | // type PartialActionContext = Partial> 15 | 16 | // interface PartialActionContext { 17 | // dispatch?: Dispatch; 18 | // commit?: Commit; 19 | // state?: S; 20 | // getters?: any; 21 | // rootState?: R; 22 | // rootGetters?: any; 23 | // } 24 | 25 | export default >{ 26 | // ensure data for rendering given list type 27 | FETCH_LIST_DATA: ({ commit, dispatch, state }: any, { type }: any) => { 28 | commit('SET_ACTIVE_TYPE', { type }) 29 | return fetchIdsByType(type) 30 | .then(ids => commit('SET_LIST', { type, ids })) 31 | .then(() => dispatch('ENSURE_ACTIVE_ITEMS')) 32 | }, 33 | 34 | // ensure all active items are fetched 35 | ENSURE_ACTIVE_ITEMS: ({ dispatch, getters }: any) => { 36 | return dispatch('FETCH_ITEMS', { 37 | ids: getters.activeIds 38 | }) 39 | }, 40 | 41 | FETCH_ITEMS: ({ commit, state }: any, { ids }: any) => { 42 | // on the client, the store itself serves as a cache. 43 | // only fetch items that we do not already have, or has expired (3 minutes) 44 | const now = Date.now() 45 | ids = ids.filter((id: any) => { 46 | const item = state.items[id] 47 | if (!item) { 48 | return true 49 | } 50 | if (now - item.__lastUpdated > 1000 * 60 * 3) { 51 | return true 52 | } 53 | return false 54 | }) 55 | if (ids.length) { 56 | return fetchItems(ids).then(items => commit('SET_ITEMS', { items })) 57 | } else { 58 | return Promise.resolve() 59 | } 60 | }, 61 | 62 | FETCH_USER: ({ commit, state }: any, { id }: any) => { 63 | return state.users[id] 64 | ? Promise.resolve(state.users[id]) 65 | : fetchUser(id).then(user => commit('SET_USER', { id, user })) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/store/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree, Getter } from 'vuex'; 2 | import { State } from './index'; 3 | 4 | 5 | export default >{ 6 | // ids of the items that should be currently displayed based on 7 | // current list type and current pagination 8 | activeIds (state: State) { 9 | const { activeType, itemsPerPage, lists } = state 10 | 11 | if (!activeType) { 12 | return [] 13 | } 14 | 15 | const page = Number(state.route.params.page) || 1 16 | const start = (page - 1) * itemsPerPage 17 | const end = page * itemsPerPage 18 | 19 | return lists[activeType].slice(start, end) 20 | }, 21 | 22 | // items that should be currently displayed. 23 | // this Array may not be fully fetched. 24 | activeItems (state: State, getters: any) { 25 | return getters.activeIds.map((id: number) => state.items[id]).filter((_: any) => _) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import actions from './actions'; 4 | import mutations from './mutations'; 5 | import getters from './getters'; 6 | 7 | import { Store } from 'vuex'; 8 | 9 | Vue.use(Vuex); 10 | 11 | export function createStore (): Store { 12 | return new Vuex.Store({ 13 | state: { 14 | activeType: null, 15 | itemsPerPage: 20, 16 | items: {/* [id: number]: Item */}, 17 | users: {/* [id: string]: User */}, 18 | lists: { 19 | top: [/* number */], 20 | new: [], 21 | show: [], 22 | ask: [], 23 | job: [] 24 | } 25 | }, 26 | actions, 27 | mutations, 28 | getters 29 | }); 30 | }; 31 | 32 | export interface Item { 33 | by: string; 34 | descendants: number; 35 | id: number; 36 | kids: Array, 37 | score: number; 38 | time: number | Date | undefined; 39 | title: string; 40 | type: string; 41 | url: string; 42 | __lastUpdated: number | Date | undefined; 43 | }; 44 | 45 | export interface User { 46 | created: number; 47 | id: string; 48 | karma: number; 49 | submitted: Array; 50 | __lastUpdated: number; 51 | }; 52 | 53 | export interface List { 54 | top: Array; 55 | new: Array; 56 | show: Array; 57 | ask: Array; 58 | job: Array; 59 | [key: string]: Array; 60 | }; 61 | 62 | export interface State { 63 | activeType: string | null; 64 | itemsPerPage: number; 65 | items: any; 66 | users: any; 67 | lists: List; 68 | route?: any; 69 | }; 70 | -------------------------------------------------------------------------------- /src/store/mutations.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { MutationTree } from 'vuex'; 4 | import { State , Item, User } from './index'; 5 | 6 | interface SetActiveTypePayload { type: string }; 7 | 8 | interface SetListPayload { type: string, ids: Array }; 9 | 10 | interface SetItemsPayload { items: Array }; 11 | 12 | interface SetUserPayload { id: number; user: User; }; 13 | 14 | export default >{ 15 | SET_ACTIVE_TYPE: (state: State, { type }: SetActiveTypePayload) => { 16 | state.activeType = type; 17 | }, 18 | 19 | SET_LIST: (state: State, { type, ids }: SetListPayload) => { 20 | state.lists[type] = ids; 21 | }, 22 | 23 | SET_ITEMS: (state: State, { items }: SetItemsPayload) => { 24 | items.forEach((item) => { 25 | if (item) { 26 | Vue.set(state.items, item.id, item); 27 | } 28 | }); 29 | }, 30 | 31 | SET_USER: (state: State, { id, user }: SetUserPayload) => { 32 | Vue.set(state.users, id, user || false); /* false means user not found */ 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/util/filters.ts: -------------------------------------------------------------------------------- 1 | export function host (url: string) { 2 | const host = url.replace(/^https?:\/\//, '').replace(/\/.*$/, '') 3 | const parts = host.split('.').slice(-3) 4 | if (parts[0] === 'www') parts.shift() 5 | return parts.join('.') 6 | } 7 | 8 | export function timeAgo (time: Date) { 9 | const between = Date.now() / 1000 - Number(time) 10 | if (between < 3600) { 11 | return pluralize(~~(between / 60), ' minute') 12 | } else if (between < 86400) { 13 | return pluralize(~~(between / 3600), ' hour') 14 | } else { 15 | return pluralize(~~(between / 86400), ' day') 16 | } 17 | } 18 | 19 | function pluralize (time: number, label: string) { 20 | if (time === 1) { 21 | return time + label 22 | } 23 | return time + label + 's' 24 | } 25 | -------------------------------------------------------------------------------- /src/util/title.ts: -------------------------------------------------------------------------------- 1 | function getTitle (vm: any) { 2 | const { title } = vm.$options 3 | if (title) { 4 | return typeof title === 'function' 5 | ? title.call(vm) 6 | : title 7 | } 8 | } 9 | 10 | const serverTitleMixin = { 11 | created () { 12 | const title = getTitle(this) 13 | if (title) { 14 | (this as any).$ssrContext.title = `Vue HN 2.0 | ${title}` 15 | } 16 | } 17 | } 18 | 19 | const clientTitleMixin = { 20 | mounted () { 21 | const title = getTitle(this) 22 | if (title) { 23 | document.title = `Vue HN 2.0 | ${title}` 24 | } 25 | } 26 | } 27 | 28 | export default process.env.VUE_ENV === 'server' 29 | ? serverTitleMixin 30 | : clientTitleMixin 31 | -------------------------------------------------------------------------------- /src/views/CreateListView.ts: -------------------------------------------------------------------------------- 1 | import ItemList from './ItemList.vue' 2 | import { Store } from 'vuex'; 3 | import { State } from '../store/index'; 4 | 5 | const camelize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1) 6 | 7 | // This is a factory function for dynamically creating root-level list views, 8 | // since they share most of the logic except for the type of items to display. 9 | // They are essentially higher order components wrapping ItemList.vue. 10 | export default function createListView (type: string) { 11 | return { 12 | name: `${type}-stories-view`, 13 | 14 | asyncData ({ store }: { store: Store }) { 15 | return store.dispatch('FETCH_LIST_DATA', { type }) 16 | }, 17 | 18 | title: camelize(type), 19 | 20 | render (h: any) { 21 | return h(ItemList, { props: { type }}) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/views/ItemList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 101 | 102 | 158 | -------------------------------------------------------------------------------- /src/views/ItemView.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 106 | 107 | 146 | -------------------------------------------------------------------------------- /src/views/UserView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | 56 | 75 | -------------------------------------------------------------------------------- /src/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | 6 | declare var System: any; 7 | 8 | declare var require: { 9 | (path: string): T; 10 | (paths: string[], callback: (...modules: any[]) => void): void; 11 | ensure: (paths: string[], callback: (require: (path:string) => T) => void) => void; 12 | }; 13 | 14 | declare var process: { 15 | env: { 16 | NODE_ENV: any, 17 | VUE_ENV: any, 18 | DEBUG_API: any 19 | }, 20 | __API__: any 21 | } 22 | 23 | interface Window { 24 | __INITIAL_STATE__: any; 25 | } 26 | 27 | declare module "create-api" 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./built/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "target": "es5", 9 | "experimentalDecorators": true, 10 | "noImplicitThis": false, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": false, 13 | "strictNullChecks": false 14 | }, 15 | "include": [ 16 | "./src/**/*" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": [ 9 | true, 10 | "single" 11 | ], 12 | "indent": [ 13 | true 14 | ], 15 | "interface-name": [ 16 | false 17 | ], 18 | "arrow-parens": false, 19 | // Pending fix for shorthand property names. 20 | "object-literal-sort-keys": false 21 | }, 22 | "rulesDirectory": [] 23 | } --------------------------------------------------------------------------------