Commit d93dd711 authored by Derek Nutile's avatar Derek Nutile

Website reboot using NuxtJS

parent c097e6bd
Pipeline #130640619 failed with stages
in 2 minutes and 12 seconds
File added
image: alpine:latest
image: node
before_script:
- npm install
cache:
paths:
- node_modules/
pages:
stage: deploy
script:
- echo 'Static pages, nothing to generate.'
- npm run generate
artifacts:
paths:
- public
- public
only:
- master
- master
import Vue from 'vue'
import {
getMatchedComponentsInstances,
getChildrenComponentInstancesUsingFetch,
promisify,
globalHandleError,
sanitizeComponent
} from './utils'
import NuxtError from '../layouts/error.vue'
import NuxtLoading from './components/nuxt-loading.vue'
import NuxtBuildIndicator from './components/nuxt-build-indicator'
import '../node_modules/vuetify/dist/vuetify.css'
import _6f6c098b from '../layouts/default.vue'
const layouts = { "_default": sanitizeComponent(_6f6c098b) }
export default {
render (h, props) {
const loadingEl = h('NuxtLoading', { ref: 'loading' })
if (this.nuxt.err && NuxtError) {
const errorLayout = (NuxtError.options || NuxtError).layout
if (errorLayout) {
this.setLayout(
typeof errorLayout === 'function'
? errorLayout.call(NuxtError, this.context)
: errorLayout
)
}
}
const layoutEl = h(this.layout || 'nuxt')
const templateEl = h('div', {
domProps: {
id: '__layout'
},
key: this.layoutName
}, [layoutEl])
const transitionEl = h('transition', {
props: {
name: 'layout',
mode: 'out-in'
},
on: {
beforeEnter (el) {
// Ensure to trigger scroll event after calling scrollBehavior
window.$nuxt.$nextTick(() => {
window.$nuxt.$emit('triggerScroll')
})
}
}
}, [templateEl])
return h('div', {
domProps: {
id: '__nuxt'
}
}, [
loadingEl,
h(NuxtBuildIndicator),
transitionEl
])
},
data: () => ({
isOnline: true,
layout: null,
layoutName: '',
nbFetching: 0
}),
beforeCreate () {
Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt)
},
created () {
// Add this.$nuxt in child instances
Vue.prototype.$nuxt = this
// add to window so we can listen when ready
if (process.client) {
window.$nuxt = this
this.refreshOnlineStatus()
// Setup the listeners
window.addEventListener('online', this.refreshOnlineStatus)
window.addEventListener('offline', this.refreshOnlineStatus)
}
// Add $nuxt.error()
this.error = this.nuxt.error
// Add $nuxt.context
this.context = this.$options.context
},
mounted () {
this.$loading = this.$refs.loading
},
watch: {
'nuxt.err': 'errorChanged'
},
computed: {
isOffline () {
return !this.isOnline
},
isFetching() {
return this.nbFetching > 0
}
},
methods: {
refreshOnlineStatus () {
if (process.client) {
if (typeof window.navigator.onLine === 'undefined') {
// If the browser doesn't support connection status reports
// assume that we are online because most apps' only react
// when they now that the connection has been interrupted
this.isOnline = true
} else {
this.isOnline = window.navigator.onLine
}
}
},
async refresh () {
const pages = getMatchedComponentsInstances(this.$route)
if (!pages.length) {
return
}
this.$loading.start()
const promises = pages.map((page) => {
const p = []
// Old fetch
if (page.$options.fetch && page.$options.fetch.length) {
p.push(promisify(page.$options.fetch, this.context))
}
if (page.$fetch) {
p.push(page.$fetch())
} else {
// Get all component instance to call $fetch
for (const component of getChildrenComponentInstancesUsingFetch(page.$vnode.componentInstance)) {
p.push(component.$fetch())
}
}
if (page.$options.asyncData) {
p.push(
promisify(page.$options.asyncData, this.context)
.then((newData) => {
for (const key in newData) {
Vue.set(page.$data, key, newData[key])
}
})
)
}
return Promise.all(p)
})
try {
await Promise.all(promises)
} catch (error) {
this.$loading.fail(error)
globalHandleError(error)
this.error(error)
}
this.$loading.finish()
},
errorChanged () {
if (this.nuxt.err && this.$loading) {
if (this.$loading.fail) {
this.$loading.fail(this.nuxt.err)
}
if (this.$loading.finish) {
this.$loading.finish()
}
}
},
setLayout (layout) {
if(layout && typeof layout !== 'string') {
throw new Error('[nuxt] Avoid using non-string value as layout property.')
}
if (!layout || !layouts['_' + layout]) {
layout = 'default'
}
this.layoutName = layout
this.layout = layouts['_' + layout]
return this.layout
},
loadLayout (layout) {
if (!layout || !layouts['_' + layout]) {
layout = 'default'
}
return Promise.resolve(layouts['_' + layout])
}
},
components: {
NuxtLoading
}
}
import Axios from 'axios'
import defu from 'defu'
// Axios.prototype cannot be modified
const axiosExtra = {
setBaseURL (baseURL) {
this.defaults.baseURL = baseURL
},
setHeader (name, value, scopes = 'common') {
for (let scope of Array.isArray(scopes) ? scopes : [ scopes ]) {
if (!value) {
delete this.defaults.headers[scope][name];
return
}
this.defaults.headers[scope][name] = value
}
},
setToken (token, type, scopes = 'common') {
const value = !token ? null : (type ? type + ' ' : '') + token
this.setHeader('Authorization', value, scopes)
},
onRequest(fn) {
this.interceptors.request.use(config => fn(config) || config)
},
onResponse(fn) {
this.interceptors.response.use(response => fn(response) || response)
},
onRequestError(fn) {
this.interceptors.request.use(undefined, error => fn(error) || Promise.reject(error))
},
onResponseError(fn) {
this.interceptors.response.use(undefined, error => fn(error) || Promise.reject(error))
},
onError(fn) {
this.onRequestError(fn)
this.onResponseError(fn)
},
create(options) {
return createAxiosInstance(defu(options, this.defaults))
}
}
// Request helpers ($get, $post, ...)
for (let method of ['request', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch']) {
axiosExtra['$' + method] = function () { return this[method].apply(this, arguments).then(res => res && res.data) }
}
const extendAxiosInstance = axios => {
for (let key in axiosExtra) {
axios[key] = axiosExtra[key].bind(axios)
}
}
const createAxiosInstance = axiosOptions => {
// Create new axios instance
const axios = Axios.create(axiosOptions)
axios.CancelToken = Axios.CancelToken
axios.isCancel = Axios.isCancel
// Extend axios proto
extendAxiosInstance(axios)
// Setup interceptors
setupProgress(axios)
return axios
}
const setupProgress = (axios) => {
if (process.server) {
return
}
// A noop loading inteterface for when $nuxt is not yet ready
const noopLoading = {
finish: () => { },
start: () => { },
fail: () => { },
set: () => { }
}
const $loading = () => (window.$nuxt && window.$nuxt.$loading && window.$nuxt.$loading.set) ? window.$nuxt.$loading : noopLoading
let currentRequests = 0
axios.onRequest(config => {
if (config && config.progress === false) {
return
}
currentRequests++
})
axios.onResponse(response => {
if (response && response.config && response.config.progress === false) {
return
}
currentRequests--
if (currentRequests <= 0) {
currentRequests = 0
$loading().finish()
}
})
axios.onError(error => {
if (error && error.config && error.config.progress === false) {
return
}
currentRequests--
if (Axios.isCancel(error)) {
return
}
$loading().fail()
$loading().finish()
})
const onProgress = e => {
if (!currentRequests) {
return
}
const progress = ((e.loaded * 100) / (e.total * currentRequests))
$loading().set(Math.min(100, progress))
}
axios.defaults.onUploadProgress = onProgress
axios.defaults.onDownloadProgress = onProgress
}
export default (ctx, inject) => {
// baseURL
const baseURL = process.browser
? 'http://localhost:3000/'
: (process.env._AXIOS_BASE_URL_ || 'http://localhost:3000/')
// Create fresh objects for all default header scopes
// Axios creates only one which is shared across SSR requests!
// https://github.com/mzabriskie/axios/blob/master/lib/defaults.js
const headers = {
"common": {
"Accept": "application/json, text/plain, */*"
},
"delete": {},
"get": {},
"head": {},
"post": {},
"put": {},
"patch": {}
}
const axiosOptions = {
baseURL,
headers
}
// Proxy SSR request headers headers
axiosOptions.headers.common = (ctx.req && ctx.req.headers) ? Object.assign({}, ctx.req.headers) : {}
delete axiosOptions.headers.common['accept']
delete axiosOptions.headers.common['host']
delete axiosOptions.headers.common['cf-ray']
delete axiosOptions.headers.common['cf-connecting-ip']
delete axiosOptions.headers.common['content-length']
delete axiosOptions.headers.common['content-md5']
delete axiosOptions.headers.common['content-type']
if (process.server) {
// Don't accept brotli encoding because Node can't parse it
axiosOptions.headers.common['accept-encoding'] = 'gzip, deflate'
}
const axios = createAxiosInstance(axiosOptions)
// Inject axios to the context as $axios
ctx.$axios = axios
inject('axios', axios)
}
This diff is collapsed.
<template>
<transition appear>
<div v-if="building" class="nuxt__build_indicator" :style="indicatorStyle">
<svg viewBox="0 0 96 72" version="1" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M6 66h23l1-3 21-37L40 6 6 66zM79 66h11L62 17l-5 9 22 37v3zM54 31L35 66h38z" />
<path d="M29 69v-1-2H6L40 6l11 20 3-6L44 3s-2-3-4-3-3 1-5 3L1 63c0 1-2 3 0 6 0 1 2 2 5 2h28c-3 0-4-1-5-2z" fill="#00C58E" />
<path d="M95 63L67 14c0-1-2-3-5-3-1 0-3 0-4 3l-4 6 3 6 5-9 28 49H79a5 5 0 0 1 0 3c-2 2-5 2-5 2h16c1 0 4 0 5-2 1-1 2-3 0-6z" fill="#00C58E" />
<path d="M79 69v-1-2-3L57 26l-3-6-3 6-21 37-1 3a5 5 0 0 0 0 3c1 1 2 2 5 2h40s3 0 5-2zM54 31l19 35H35l19-35z" fill="#FFF" fill-rule="nonzero" />
</g>
</svg>
{{ animatedProgress }}%
</div>
</transition>
</template>
<script>
export default {
name: 'NuxtBuildIndicator',
data () {
return {
building: false,
progress: 0,
animatedProgress: 0,
reconnectAttempts: 0
}
},
computed: {
options: () => ({"position":"bottom-right","backgroundColor":"#2E495E","color":"#00C48D"}),
indicatorStyle () {
const [d1, d2] = this.options.position.split('-')
return {
[d1]: '20px',
[d2]: '20px',
'background-color': this.options.backgroundColor,
color: this.options.color
}
}
},
watch: {
progress (val, oldVal) {
// Average progress may decrease but ignore it!
if (val < oldVal) {
return
}
// Cancel old animation
clearInterval(this._progressAnimation)
// Jump to edge immediately
if (val < 10 || val > 90) {
this.animatedProgress = val
return
}
// Animate to value
this._progressAnimation = setInterval(() => {
const diff = this.progress - this.animatedProgress
if (diff > 0) {
this.animatedProgress++
} else {
clearInterval(this._progressAnimation)
}
}, 50)
}
},
mounted () {
if (EventSource === undefined) {
return // Unsupported
}
this.sseConnect()
},
beforeDestroy () {
this.sseClose()
clearInterval(this._progressAnimation)
},
methods: {
sseConnect () {
if (this._connecting) {
return
}
this._connecting = true
this.sse = new EventSource('/_loading/sse')
this.sse.addEventListener('message', event => this.onSseMessage(event))
},
onSseMessage (message) {
const data = JSON.parse(message.data)
if (!data.states) {
return
}
this.progress = Math.round(data.states.reduce((p, s) => p + s.progress, 0) / data.states.length)
if (!data.allDone) {
this.building = true
} else {
this.$nextTick(() => {
this.building = false
this.animatedProgress = 0
this.progress = 0
clearInterval(this._progressAnimation)
})
}
},
sseClose () {
if (this.sse) {
this.sse.close()
delete this.sse
}
}
}
}
</script>
<style scoped>
.nuxt__build_indicator {
box-sizing: border-box;
position: fixed;
font-family: monospace;
padding: 5px 10px;
border-radius: 5px;
box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.2);
width: 88px;
z-index: 2147483647;
font-size: 16px;
line-height: 1.2rem;
}
.v-enter-active, .v-leave-active {
transition-delay: 0.2s;
transition-property: all;
transition-duration: 0.3s;
}
.v-leave-to {
opacity: 0;
transform: translateY(20px);
}
svg {
display: inline-block;
vertical-align: baseline;
width: 1.1em;
position: relative;
top: 1px;
}
</style>
export default {
name: 'NuxtChild',
functional: true,
props: {
nuxtChildKey: {
type: String,
default: ''
},
keepAlive: Boolean,
keepAliveProps: {
type: Object,
default: undefined
}
},
render (_, { parent, data, props }) {
const h = parent.$createElement
data.nuxtChild = true
const _parent = parent
const transitions = parent.$nuxt.nuxt.transitions
const defaultTransition = parent.$nuxt.nuxt.defaultTransition
let depth = 0
while (parent) {
if (parent.$vnode && parent.$vnode.data.nuxtChild) {
depth++
}
parent = parent.$parent
}
data.nuxtChildDepth = depth
const transition = transitions[depth] || defaultTransition
const transitionProps = {}
transitionsKeys.forEach((key) => {
if (typeof transition[key] !== 'undefined') {
transitionProps[key] = transition[key]
}
})
const listeners = {}
listenersKeys.forEach((key) => {
if (typeof transition[key] === 'function') {
listeners[key] = transition[key].bind(_parent)
}
})
if (process.client) {
// Add triggerScroll event on beforeEnter (fix #1376)
const beforeEnter = listeners.beforeEnter
listeners.beforeEnter = (el) => {
// Ensure to trigger scroll event after calling scrollBehavior
window.$nuxt.$nextTick(() => {
window.$nuxt.$emit('triggerScroll')
})
if (beforeEnter) {
return beforeEnter.call(_parent, el)
}
}
}
// make sure that leave is called asynchronous (fix #5703)
if (transition.css === false) {
const leave = listeners.leave
// only add leave listener when user didnt provide one
// or when it misses the done argument
if (!leave || leave.length < 2) {
listeners.leave = (el, done) => {
if (leave) {
leave.call(_parent, el)
}
_parent.$nextTick(done)
}
}
}
let routerView = h('routerView', data)
if (props.keepAlive) {
routerView = h('keep-alive', { props: props.keepAliveProps }, [routerView])
}