Stuck at changing routes

This commit is contained in:
Julien Nahum 2023-10-14 18:24:44 +02:00
parent 358bf0e4d0
commit 3d3759c58c
29 changed files with 316 additions and 540 deletions

2
resources/js/app.js vendored
View File

@ -23,6 +23,6 @@ configureCompat({
}) })
router.app = app router.app = app
app.mount('#app') router.isReady().then(() => app.mount('#app'))
export default app export default app

View File

@ -1,41 +0,0 @@
<template>
<Dropdown v-if="Object.keys(locales).length > 1"
dropdown-class="origin-top-right absolute right-0 mt-2 w-20 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5">
<template #trigger="{toggle}">
<a class="text-gray-300 hover:text-gray-800 dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium" href="#" role="button" @click.prevent="toggle"
>
{{ locales[locale] }}
</a>
</template>
<a v-for="(value, key) in locales" :key="key" class="block block text-center px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center z-10" href="#"
@click.prevent="setLocale(key)"
>
{{ value }}
</a>
</Dropdown>
</template>
<script>
import { mapGetters } from 'vuex'
import { loadMessages } from '~/plugins/i18n.js'
import Dropdown from './common/Dropdown.vue'
export default {
components: { Dropdown },
computed: mapGetters({
locale: 'lang/locale',
locales: 'lang/locales'
}),
methods: {
setLocale (locale) {
if (this.$i18n.locale !== locale) {
loadMessages(locale)
this.$store.dispatch('lang/setLocale', { locale })
}
}
}
}
</script>

View File

@ -1,11 +1,13 @@
<template> <template>
<v-button v-if="githubAuth" color="gray" type="button" @click="login"> <v-button v-if="githubAuth" color="gray" type="button" @click="login">
<div class="flex justify-center"> <div class="flex justify-center">
{{ $t('login_with') }} Login with
<svg class="w-6 h-6 text-white ml-2" xmlns="http://www.w3.org/2000/svg" <svg class="w-6 h-6 text-white ml-2" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"> viewBox="0 0 24 24"
>
<path <path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/> d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg> </svg>
</div> </div>
</v-button> </v-button>
@ -24,13 +26,13 @@ export default {
window.addEventListener('message', this.onMessage, false) window.addEventListener('message', this.onMessage, false)
}, },
beforeDestroy () { beforeUnmount () {
window.removeEventListener('message', this.onMessage) window.removeEventListener('message', this.onMessage)
}, },
methods: { methods: {
async login () { async login () {
const newWindow = openWindow('', this.$t('login')) const newWindow = openWindow('', 'Login')
const url = await this.$store.dispatch('auth/fetchOauthUrl', { const url = await this.$store.dispatch('auth/fetchOauthUrl', {
provider: 'github' provider: 'github'

View File

@ -128,7 +128,7 @@ export default {
document.addEventListener('keydown', closeOnEscape) document.addEventListener('keydown', closeOnEscape)
this.$once('hook:destroyed', () => { this.$once('hook:unmounted', () => {
document.removeEventListener('keydown', closeOnEscape) document.removeEventListener('keydown', closeOnEscape)
}) })
}, },

View File

@ -13,31 +13,34 @@
<workspace-dropdown class="ml-6" /> <workspace-dropdown class="ml-6" />
</div> </div>
<div v-if="showAuth" class="hidden md:block ml-auto relative"> <div v-if="showAuth" class="hidden md:block ml-auto relative">
<router-link :to="{name:'templates'}" v-if="$route.name !== 'templates'" <router-link v-if="$route.name !== 'templates'" :to="{name:'templates'}"
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"> class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"
>
Templates Templates
</router-link> </router-link>
<router-link :to="{name:'aiformbuilder'}" v-if="$route.name !== 'aiformbuilder'" <router-link v-if="$route.name !== 'aiformbuilder'" :to="{name:'aiformbuilder'}"
class="text-sm text-gray-600 dark:text-white hidden lg:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"> class="text-sm text-gray-600 dark:text-white hidden lg:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
>
AI Form Builder AI Form Builder
</router-link> </router-link>
<router-link :to="{name:'pricing'}" v-if="paidPlansEnabled && (user===null || (user && workspace && !workspace.is_pro)) && $route.name !== 'pricing'" <router-link v-if="paidPlansEnabled && (user===null || (user && workspace && !workspace.is_pro)) && $route.name !== 'pricing'" :to="{name:'pricing'}"
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"> class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"
>
<span v-if="user">Upgrade</span> <span v-if="user">Upgrade</span>
<span v-else>Pricing</span> <span v-else>Pricing</span>
</router-link> </router-link>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" <a v-if="hasCrisp" href="#"
@click.prevent="openCrisp" v-if="hasCrisp" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" @click.prevent="openCrisp"
> >
Help Help
</a> </a>
<a :href="helpUrl" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" <a v-else :href="helpUrl"
target="_blank" v-else class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" target="_blank"
> >
Help Help
</a> </a>
</div> </div>
<div v-if="showAuth" class="hidden md:block pl-5 border-gray-300 border-r h-5"></div> <div v-if="showAuth" class="hidden md:block pl-5 border-gray-300 border-r h-5" />
<div v-if="showAuth" class="block"> <div v-if="showAuth" class="block">
<div class="flex items-center"> <div class="flex items-center">
<div class="ml-3 mr-4 relative"> <div class="ml-3 mr-4 relative">
@ -90,7 +93,7 @@
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/> />
</svg> </svg>
{{ $t('settings') }} Settings
</router-link> </router-link>
<a href="#" <a href="#"
@ -104,21 +107,20 @@
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/> />
</svg> </svg>
{{ $t('logout') }} Logout
</a> </a>
</dropdown> </dropdown>
<div class="flex gap-2" v-else> <div v-else class="flex gap-2">
<router-link v-if="$route.name !== 'login'" :to="{ name: 'login' }" <router-link v-if="$route.name !== 'login'" :to="{ name: 'login' }"
class="text-gray-600 dark:text-white hover:text-gray-800 dark:hover:text-white px-0 sm:px-3 py-2 rounded-md text-sm" class="text-gray-600 dark:text-white hover:text-gray-800 dark:hover:text-white px-0 sm:px-3 py-2 rounded-md text-sm"
active-class="text-gray-800 dark:text-white" active-class="text-gray-800 dark:text-white"
> >
{{ $t('login') }} Login
</router-link> </router-link>
<v-button size="small" :to="{ name: 'forms.create.guest' }" color="outline-blue" v-track.nav_create_form_click :arrow="true"> <v-button v-track.nav_create_form_click size="small" :to="{ name: 'forms.create.guest' }" color="outline-blue" :arrow="true">
Create a form Create a form
</v-button> </v-button>
</div> </div>
</div> </div>
</div> </div>
@ -141,7 +143,7 @@ export default {
}, },
data: () => ({ data: () => ({
appName: window.config.appName, appName: window.config.appName
}), }),
computed: { computed: {
@ -188,7 +190,7 @@ export default {
}, },
hasCrisp () { hasCrisp () {
return window.config.crisp_website_id return window.config.crisp_website_id
}, }
}, },
methods: { methods: {

View File

@ -1,95 +0,0 @@
<template>
<transition enter-active-class="linear duration-500 overflow-hidden"
enter-from-class="max-h-0 opacity-0"
enter-to-class="max-h-screen opacity-100"
leave-active-class="linear duration-500 overflow-hidden"
leave-from-class="max-h-screen opacity-100"
leave-to-class="max-h-0 opacity-0"
>
<div :class="alertClasses" class="border shadow-sm p-2 flex items-center rounded-md">
<div class="flex-grow">
<p class="mb-0 py-2 px-4" :class="textClasses" v-html="message" />
</div>
<div class="justify-end">
<v-button v-if="type == 'error'" color="red" shade="light" @click="close">
Close
</v-button>
<v-button v-if="type == 'success'" color="green" shade="light" @click="close">
OK
</v-button>
<v-button v-if="type == 'warning'" color="yellow" shade="light" @click="close">
OK
</v-button>
<v-button v-if="type == 'confirmation'" class="mr-1 mb-1" @click="confirm">
Yes
</v-button>
<v-button v-if="type == 'confirmation'" color="gray" shade="light" @click="cancel">
No, cancel
</v-button>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'Alert',
props: ['type', 'message', 'autoClose', 'confirmationProceed', 'confirmationCancel'],
data () {
return {
timeout: null
}
},
computed: {
alertClasses () {
if (this.type === 'error') return 'bg-red-100 border-red-500'
if (this.type === 'success') return 'bg-green-100 border-green-500'
if (this.type === 'warning') return 'bg-yellow-100 border-yellow-500'
return 'bg-blue-50 border-nt-blue-light'
},
textClasses () {
if (this.type === 'error') return 'text-red-600'
if (this.type === 'success') return 'text-green-600'
if (this.type === 'warning') return 'text-yellow-600'
return 'text-nt-blue'
}
},
mounted () {
if (this.autoClose) {
this.timeout = setTimeout(() => {
this.close()
}, this.autoClose)
}
},
methods: {
/**
* Close the modal.
*/
close () {
clearTimeout(this.timeout)
this.$emit('close')
},
/**
* Confirm and close the modal.
*/
confirm () {
this.confirmationProceed()
this.close()
},
/**
* Cancel and close the modal.
*/
cancel () {
if (this.confirmationCancel) {
this.confirmationCancel()
}
this.close()
}
}
}
</script>

View File

@ -60,19 +60,19 @@ export default {
const scrollContainerObserver = newResizeObserver(this.toggleShadow) const scrollContainerObserver = newResizeObserver(this.toggleShadow)
if (scrollContainerObserver) { if (scrollContainerObserver) {
scrollContainerObserver.observe(this.$refs.scrollContainer) scrollContainerObserver.observe(this.$refs.scrollContainer)
// Cleanup when the component is destroyed. // Cleanup when the component is unmounted.
this.$once('hook:destroyed', () => scrollContainerObserver.disconnect()) this.$once('hook:unmounted', () => scrollContainerObserver.disconnect())
} }
// Recalculate the container dimensions when the wrapper is resized. // Recalculate the container dimensions when the wrapper is resized.
const wrapObserver = newResizeObserver(this.calcDimensions) const wrapObserver = newResizeObserver(this.calcDimensions)
if (wrapObserver) { if (wrapObserver) {
wrapObserver.observe(this.$el) wrapObserver.observe(this.$el)
// Cleanup when the component is destroyed. // Cleanup when the component is unmounted.
this.$once('hook:destroyed', () => wrapObserver.disconnect()) this.$once('hook:unmounted', () => wrapObserver.disconnect())
} }
}, },
destroyed () { unmounted () {
window.removeEventListener('resize', this.calcDimensions) window.removeEventListener('resize', this.calcDimensions)
}, },
methods: { methods: {

View File

@ -216,7 +216,7 @@ export default {
this.$root.hideNavbar() this.$root.hideNavbar()
}, },
beforeDestroy () { beforeUnmount () {
this.$root.hideNavbar(false) this.$root.hideNavbar(false)
}, },

View File

@ -22,7 +22,8 @@
</resizable-th> </resizable-th>
<th v-if="hasActions" class="n-table-cell p-0 relative" style="width: 100px"> <th v-if="hasActions" class="n-table-cell p-0 relative" style="width: 100px">
<p <p
class="bg-gray-50 dark:bg-notion-dark truncate sticky top-0 border-b border-gray-200 dark:border-gray-800 px-4 py-2 text-gray-500 font-semibold tracking-wider uppercase text-xs"> class="bg-gray-50 dark:bg-notion-dark truncate sticky top-0 border-b border-gray-200 dark:border-gray-800 px-4 py-2 text-gray-500 font-semibold tracking-wider uppercase text-xs"
>
Actions Actions
</p> </p>
</th> </th>
@ -115,7 +116,7 @@ export default {
required: false, required: false,
default: true, default: true,
type: Boolean type: Boolean
}, }
}, },
data () { data () {
@ -150,9 +151,9 @@ export default {
url: OpenUrl, url: OpenUrl,
email: OpenText, email: OpenText,
phone_number: OpenText, phone_number: OpenText,
signature: OpenFile, signature: OpenFile
}
} }
},
}, },
watch: { watch: {
@ -178,7 +179,7 @@ export default {
this.handleScroll() this.handleScroll()
}, },
beforeDestroy() { beforeUnmount () {
const parent = document.getElementById('table-page') const parent = document.getElementById('table-page')
if (parent) { if (parent) {
parent.removeEventListener('scroll', this.handleScroll) parent.removeEventListener('scroll', this.handleScroll)
@ -264,7 +265,7 @@ export default {
} }
} }
} }
}, }
} }
} }
</script> </script>

View File

@ -2,49 +2,20 @@
<div class="main-layout min-h-screen flex flex-col"> <div class="main-layout min-h-screen flex flex-col">
<navbar /> <navbar />
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<alert v-if="alert.type"
class="my-4"
:message="alert.message"
:type="alert.type"
:auto-close="alert.autoClose"
:confirmation-proceed="alert.confirmationProceed"
:confirmation-cancel="alert.confirmationCancel"
@close="closeAlert"
/>
</div>
<child class="flex-grow" /> <child class="flex-grow" />
</div> </div>
</template> </template>
<script> <script>
import Navbar from '~/components/Navbar.vue' import Navbar from '~/components/Navbar.vue'
import Alert from '../components/common/Alert.vue'
export default { export default {
name: 'MainLayout', name: 'MainLayout',
components: { components: {
Navbar, Alert Navbar
}, },
computed: { computed: {}
alert () {
return this.$root.alert
}
},
methods: {
closeAlert () {
this.$root.alert = {
type: null,
autoClose: 0,
message: '',
confirmationProceed: null,
confirmationCancel: null
}
}
}
} }
</script> </script>

View File

@ -1,8 +0,0 @@
import store from '~/store'
import { loadMessages } from '~/plugins/i18n'
export default async (to, from, next) => {
await loadMessages(store.getters['lang/locale'])
next()
}

View File

@ -2,35 +2,39 @@
<div> <div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" /> <forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<form @submit.prevent="login" @keydown="form.onKeydown($event)" class="mt-4"> <form class="mt-4" @submit.prevent="login" @keydown="form.onKeydown($event)">
<!-- Email --> <!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" /> <text-input name="email" :form="form" label="Email" :required="true" placeholder="Your email address" />
<!-- Password --> <!-- Password -->
<text-input native-type="password" placeholder="Your password" <text-input native-type="password" placeholder="Your password"
name="password" :form="form" :label="$t('password')" :required="true" name="password" :form="form" label="Password" :required="true"
/> />
<!-- Remember Me --> <!-- Remember Me -->
<div class="relative flex items-center my-5"> <div class="relative flex items-center my-5">
<v-checkbox v-model="remember" class="w-full md:w-1/2" name="remember" size="small"> <v-checkbox v-model="remember" class="w-full md:w-1/2" name="remember" size="small">
{{ $t('remember_me') }} Remember me
</v-checkbox> </v-checkbox>
<div class="w-full md:w-1/2 text-right"> <div class="w-full md:w-1/2 text-right">
<a href="#" @click.prevent="showForgotModal=true" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700"> <a href="#" class="text-xs hover:underline text-gray-500 sm:text-sm hover:text-gray-700" @click.prevent="showForgotModal=true">
Forgot your password? Forgot your password?
</a> </a>
</div> </div>
</div> </div>
<!-- Submit Button --> <!-- Submit Button -->
<v-button dusk="btn_login" :loading="form.busy">Log in to continue</v-button> <v-button dusk="btn_login" :loading="form.busy">
Log in to continue
</v-button>
<p class="text-gray-500 mt-4"> <p class="text-gray-500 mt-4">
Don't have an account? Don't have an account?
<a href="#" v-if="isQuick" @click.prevent="$emit('openRegister')" class="font-semibold ml-1">Sign Up</a> <a v-if="isQuick" href="#" class="font-semibold ml-1" @click.prevent="$emit('openRegister')">Sign Up</a>
<router-link v-else :to="{name:'register'}" class="font-semibold ml-1">Sign Up</router-link> <router-link v-else :to="{name:'register'}" class="font-semibold ml-1">
Sign Up
</router-link>
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,11 +1,11 @@
<template> <template>
<div> <div>
<form @submit.prevent="register" @keydown="form.onKeydown($event)" class="mt-4"> <form class="mt-4" @submit.prevent="register" @keydown="form.onKeydown($event)">
<!-- Name --> <!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" placeholder="Your name" :required="true" /> <text-input name="name" :form="form" label="Name" placeholder="Your name" :required="true" />
<!-- Email --> <!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" /> <text-input name="email" :form="form" label="Email" :required="true" placeholder="Your email address" />
<select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option" <select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option"
label="How did you hear about us?" :required="true" label="How did you hear about us?" :required="true"
@ -13,27 +13,35 @@
<!-- Password --> <!-- Password -->
<text-input native-type="password" placeholder="Enter password" <text-input native-type="password" placeholder="Enter password"
name="password" :form="form" :label="$t('password')" :required="true" name="password" :form="form" label="Password" :required="true"
/> />
<!-- Password Confirmation--> <!-- Password Confirmation-->
<text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password" <text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password"
name="password_confirmation" :label="$t('confirm_password')" name="password_confirmation" label="Confirm Password"
/> />
<checkbox-input :form="form" name="agree_terms" :required="true"> <checkbox-input :form="form" name="agree_terms" :required="true">
<template #label> <template #label>
I agree with the <router-link :to="{name:'terms-conditions'}" target="_blank">Terms and conditions</router-link> and <router-link :to="{name:'privacy-policy'}" target="_blank">Privacy policy</router-link> of the website and I accept them. I agree with the <router-link :to="{name:'terms-conditions'}" target="_blank">
Terms and conditions
</router-link> and <router-link :to="{name:'privacy-policy'}" target="_blank">
Privacy policy
</router-link> of the website and I accept them.
</template> </template>
</checkbox-input> </checkbox-input>
<!-- Submit Button --> <!-- Submit Button -->
<v-button :loading="form.busy">Create an account</v-button> <v-button :loading="form.busy">
Create an account
</v-button>
<p class="text-gray-500 mt-4"> <p class="text-gray-500 mt-4">
Already have an account? Already have an account?
<a href="#" v-if="isQuick" @click.prevent="$emit('openLogin')" class="font-semibold ml-1">Log In</a> <a v-if="isQuick" href="#" class="font-semibold ml-1" @click.prevent="$emit('openLogin')">Log In</a>
<router-link v-else :to="{name:'login'}" class="font-semibold ml-1">Log In</router-link> <router-link v-else :to="{name:'login'}" class="font-semibold ml-1">
Log In
</router-link>
</p> </p>
<!-- GitHub Register Button --> <!-- GitHub Register Button -->
@ -52,7 +60,7 @@ export default {
name: 'RegisterForm', name: 'RegisterForm',
components: { components: {
SelectInput, SelectInput,
LoginWithGithub, LoginWithGithub
}, },
props: { props: {
isQuick: { isQuick: {

View File

@ -3,17 +3,17 @@
<div class="flex mt-6 mb-10"> <div class="flex mt-6 mb-10">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4"> <div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4">
<h1 class="my-6"> <h1 class="my-6">
{{ $t('reset_password') }} Reset password
</h1> </h1>
<form @submit.prevent="send" @keydown="form.onKeydown($event)"> <form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status" class="mb-4" /> <alert-success :form="form" :message="status" class="mb-4" />
<!-- Email --> <!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" /> <text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button --> <!-- Submit Button -->
<v-button class="w-full" :loading="form.busy"> <v-button class="w-full" :loading="form.busy">
{{ $t('send_password_reset_link') }} Send Password Reset Link
</v-button> </v-button>
</form> </form>
</div> </div>
@ -28,12 +28,12 @@ import OpenFormFooter from '../../../components/pages/OpenFormFooter.vue'
import SeoMeta from '../../../mixins/seo-meta.js' import SeoMeta from '../../../mixins/seo-meta.js'
export default { export default {
middleware: 'guest',
components: { components: {
OpenFormFooter OpenFormFooter
}, },
mixins: [SeoMeta], mixins: [SeoMeta],
middleware: 'guest',
data: () => ({ data: () => ({
metaTitle: 'Reset Password', metaTitle: 'Reset Password',

View File

@ -3,27 +3,27 @@
<div class="flex mt-6 mb-10"> <div class="flex mt-6 mb-10">
<div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4"> <div class="w-full md:w-2/3 md:mx-auto md:max-w-md px-4">
<h1 class="my-6"> <h1 class="my-6">
{{ $t('reset_password') }} Reset Password
</h1> </h1>
<form @submit.prevent="reset" @keydown="form.onKeydown($event)"> <form @submit.prevent="reset" @keydown="form.onKeydown($event)">
<alert-success class="mb-4" :form="form" :message="status" /> <alert-success class="mb-4" :form="form" :message="status" />
<!-- Email --> <!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" /> <text-input name="email" :form="form" label="Email" :required="true" />
<!-- Password --> <!-- Password -->
<text-input class="mt-8" native-type="password" <text-input class="mt-8" native-type="password"
name="password" :form="form" :label="$t('password')" :required="true" name="password" :form="form" label="Password" :required="true"
/> />
<!-- Password Confirmation--> <!-- Password Confirmation-->
<text-input class="mt-8" native-type="password" <text-input class="mt-8" native-type="password"
name="password_confirmation" :form="form" :label="$t('confirm_password')" :required="true" name="password_confirmation" :form="form" label="Confirm Password" :required="true"
/> />
<!-- Submit Button --> <!-- Submit Button -->
<v-button class="w-full" :loading="form.busy"> <v-button class="w-full" :loading="form.busy">
{{ $t('reset_password') }} Reset Password
</v-button> </v-button>
</form> </form>
</div> </div>
@ -38,12 +38,12 @@ import OpenFormFooter from '../../../components/pages/OpenFormFooter.vue'
import SeoMeta from '../../../mixins/seo-meta.js' import SeoMeta from '../../../mixins/seo-meta.js'
export default { export default {
middleware: 'guest',
components: { components: {
OpenFormFooter OpenFormFooter
}, },
mixins: [SeoMeta], mixins: [SeoMeta],
middleware: 'guest',
data: () => ({ data: () => ({
metaTitle: 'Reset Password', metaTitle: 'Reset Password',

View File

@ -2,19 +2,19 @@
<div class="row"> <div class="row">
<div class="col-lg-8 m-auto px-4"> <div class="col-lg-8 m-auto px-4">
<h1 class="my-6"> <h1 class="my-6">
{{ $t('verify_email') }} Verify Email
</h1> </h1>
<form @submit.prevent="send" @keydown="form.onKeydown($event)"> <form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status" /> <alert-success :form="form" :message="status" />
<!-- Email --> <!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" /> <text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button --> <!-- Submit Button -->
<div class="form-group row"> <div class="form-group row">
<div class="col-md-9 ml-md-auto"> <div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy"> <v-button :loading="form.busy">
{{ $t('send_verification_link') }} Send Verification Link
</v-button> </v-button>
</div> </div>
</div> </div>
@ -28,8 +28,8 @@ import Form from 'vform'
import SeoMeta from '../../../mixins/seo-meta.js' import SeoMeta from '../../../mixins/seo-meta.js'
export default { export default {
middleware: 'guest',
mixins: [SeoMeta], mixins: [SeoMeta],
middleware: 'guest',
data: () => ({ data: () => ({
metaTitle: 'Verify Email', metaTitle: 'Verify Email',

View File

@ -2,7 +2,7 @@
<div class="row"> <div class="row">
<div class="col-lg-8 m-auto px-4"> <div class="col-lg-8 m-auto px-4">
<h1 class="my-6"> <h1 class="my-6">
{{ $t('verify_email') }} Verify Email
</h1> </h1>
<template v-if="success"> <template v-if="success">
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">
@ -10,16 +10,16 @@
</div> </div>
<router-link :to="{ name: 'login' }" class="btn btn-primary"> <router-link :to="{ name: 'login' }" class="btn btn-primary">
{{ $t('login') }} Login
</router-link> </router-link>
</template> </template>
<template v-else> <template v-else>
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
{{ error || $t('failed_to_verify_email') }} {{ error || 'Failed to verify email.' }}
</div> </div>
<router-link :to="{ name: 'verification.resend' }" class="small float-right"> <router-link :to="{ name: 'verification.resend' }" class="small float-right">
{{ $t('resend_verification_link') }} Resend Verification Link?
</router-link> </router-link>
</template> </template>
</div> </div>
@ -33,6 +33,7 @@ import SeoMeta from '../../../mixins/seo-meta.js'
const qs = (params) => Object.keys(params).map(key => `${key}=${params[key]}`).join('&') const qs = (params) => Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
export default { export default {
mixins: [SeoMeta],
async beforeRouteEnter (to, from, next) { async beforeRouteEnter (to, from, next) {
try { try {
const { data } = await axios.post(`/api/email/verify/${to.params.id}?${qs(to.query)}`) const { data } = await axios.post(`/api/email/verify/${to.params.id}?${qs(to.query)}`)
@ -48,7 +49,6 @@ export default {
}, },
middleware: 'guest', middleware: 'guest',
mixins: [SeoMeta],
data: () => ({ data: () => ({
metaTitle: 'Verify Email', metaTitle: 'Verify Email',

View File

@ -4,12 +4,12 @@
<img alt="Nice plant as we have nothing else to show!" :src="asset('img/icons/plant.png')" class="w-56 mb-5"> <img alt="Nice plant as we have nothing else to show!" :src="asset('img/icons/plant.png')" class="w-56 mb-5">
<h1 class="mb-4 font-semibold text-3xl text-gray-900"> <h1 class="mb-4 font-semibold text-3xl text-gray-900">
{{ $t('page_not_found') }} Page Not Found
</h1> </h1>
<div class="links"> <div class="links">
<router-link :to="{ name: 'welcome' }" class="hover:underline text-gray-700"> <router-link :to="{ name: 'welcome' }" class="hover:underline text-gray-700">
{{ $t('go_home') }} Go Home
</router-link> </router-link>
</div> </div>
</div> </div>

View File

@ -2,12 +2,13 @@
<div class="flex flex-wrap flex-col"> <div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in"> <transition v-if="stateReady" name="fade" mode="out-in">
<div key="2"> <div key="2">
<create-form-base-modal @form-generated="formGenerated" :show="showInitialFormModal" <create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"/> @close="showInitialFormModal=false"
/>
<form-editor v-if="!workspacesLoading" ref="editor" <form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow" class="w-full flex flex-grow"
:error="error" :error="error"
:isGuest="isGuest" :is-guest="isGuest"
@openRegister="openRegister" @openRegister="openRegister"
/> />
<div v-else class="text-center mt-4 py-6"> <div v-else class="text-center mt-4 py-6">
@ -16,9 +17,9 @@
</div> </div>
</transition> </transition>
<quick-register :showRegisterModal="registerModal" @close="registerModal=false" @reopen="registerModal=true" <quick-register :show-register-modal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
@afterLogin="afterLogin"/> @afterLogin="afterLogin"
/>
</div> </div>
</template> </template>
@ -27,9 +28,9 @@ import store from '~/store'
import Form from 'vform' import Form from 'vform'
import { mapState, mapActions } from 'vuex' import { mapState, mapActions } from 'vuex'
import QuickRegister from '../auth/components/QuickRegister.vue' import QuickRegister from '../auth/components/QuickRegister.vue'
import initForm from "../../mixins/form_editor/initForm.js" import initForm from '../../mixins/form_editor/initForm.js'
import SeoMeta from '../../mixins/seo-meta.js' import SeoMeta from '../../mixins/seo-meta.js'
import CreateFormBaseModal from "../../components/pages/forms/create/CreateFormBaseModal.vue" import CreateFormBaseModal from '../../components/pages/forms/create/CreateFormBaseModal.vue'
const loadTemplates = function () { const loadTemplates = function () {
store.commit('open/templates/startLoading') store.commit('open/templates/startLoading')
@ -40,18 +41,18 @@ const loadTemplates = function () {
export default { export default {
name: 'CreateFormGuest', name: 'CreateFormGuest',
mixins: [initForm, SeoMeta],
components: { components: {
QuickRegister, CreateFormBaseModal QuickRegister, CreateFormBaseModal
}, },
mixins: [initForm, SeoMeta],
middleware: 'guest',
beforeRouteEnter (to, from, next) { beforeRouteEnter (to, from, next) {
loadTemplates() loadTemplates()
next() next()
}, },
middleware: 'guest',
data () { data () {
return { return {
metaTitle: 'Create a new Form as Guest', metaTitle: 'Create a new Form as Guest',
@ -67,7 +68,7 @@ export default {
computed: { computed: {
...mapState({ ...mapState({
workspaces: state => state['open/workspaces'].content, workspaces: state => state['open/workspaces'].content,
workspacesLoading: state => state['open/workspaces'].loading, workspacesLoading: state => state['open/workspaces'].loading
}), }),
form: { form: {
get () { get () {
@ -80,7 +81,7 @@ export default {
}, },
workspace () { workspace () {
return this.$store.getters['open/workspaces/getCurrent']() return this.$store.getters['open/workspaces/getCurrent']()
}, }
}, },
watch: { watch: {
@ -95,7 +96,7 @@ export default {
// Set as guest user // Set as guest user
const guestWorkspace = { const guestWorkspace = {
id: null, id: null,
name: "Guest Workspace", name: 'Guest Workspace',
is_enterprise: false, is_enterprise: false,
is_pro: false is_pro: false
} }
@ -117,7 +118,7 @@ export default {
}, },
created () {}, created () {},
destroyed() {}, unmounted () {},
methods: { methods: {
...mapActions({ ...mapActions({

View File

@ -2,8 +2,9 @@
<div class="flex flex-wrap flex-col"> <div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in"> <transition v-if="stateReady" name="fade" mode="out-in">
<div key="2"> <div key="2">
<create-form-base-modal @form-generated="formGenerated" :show="showInitialFormModal" <create-form-base-modal :show="showInitialFormModal" @form-generated="formGenerated"
@close="showInitialFormModal=false"/> @close="showInitialFormModal=false"
/>
<form-editor v-if="!workspacesLoading" ref="editor" <form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow" class="w-full flex flex-grow"
:error="error" :error="error"
@ -21,9 +22,9 @@
import store from '~/store' import store from '~/store'
import Form from 'vform' import Form from 'vform'
import { mapState, mapActions } from 'vuex' import { mapState, mapActions } from 'vuex'
import initForm from "../../mixins/form_editor/initForm.js"; import initForm from '../../mixins/form_editor/initForm.js'
import SeoMeta from '../../mixins/seo-meta.js' import SeoMeta from '../../mixins/seo-meta.js'
import CreateFormBaseModal from "../../components/pages/forms/create/CreateFormBaseModal.vue" import CreateFormBaseModal from '../../components/pages/forms/create/CreateFormBaseModal.vue'
const loadTemplates = function () { const loadTemplates = function () {
store.commit('open/templates/startLoading') store.commit('open/templates/startLoading')
@ -34,9 +35,9 @@ const loadTemplates = function () {
export default { export default {
name: 'CreateForm', name: 'CreateForm',
components: { CreateFormBaseModal },
mixins: [initForm, SeoMeta], mixins: [initForm, SeoMeta],
components: {CreateFormBaseModal},
beforeRouteEnter (to, from, next) { beforeRouteEnter (to, from, next) {
loadTemplates() loadTemplates()
@ -83,7 +84,7 @@ export default {
}, },
workspace () { workspace () {
return this.$store.getters['open/workspaces/getCurrent']() return this.$store.getters['open/workspaces/getCurrent']()
}, }
}, },
watch: { watch: {
@ -122,7 +123,7 @@ export default {
}, },
created () {}, created () {},
destroyed() {}, unmounted () {},
methods: { methods: {
...mapActions({ ...mapActions({

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<form-editor v-if="pageLoaded" ref="editor" <form-editor v-if="pageLoaded" ref="editor"
:isEdit="true" :is-edit="true"
@on-save="formInitialHash=null" @on-save="formInitialHash=null"
/> />
<div v-else-if="!loading && error" class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500"> <div v-else-if="!loading && error" class="mt-4 rounded-lg max-w-xl mx-auto p-6 bg-red-100 text-red-500">
@ -31,6 +31,7 @@ const loadForms = function () {
export default { export default {
name: 'EditForm', name: 'EditForm',
components: { Breadcrumb }, components: { Breadcrumb },
mixins: [SeoMeta],
beforeRouteEnter (to, from, next) { beforeRouteEnter (to, from, next) {
if (!store.getters['open/forms/getBySlug'](to.params.slug)) { if (!store.getters['open/forms/getBySlug'](to.params.slug)) {
@ -51,7 +52,6 @@ export default {
}, },
middleware: 'auth', middleware: 'auth',
mixins: [SeoMeta],
data () { data () {
return { return {
@ -82,7 +82,7 @@ export default {
}, },
metaTitle () { metaTitle () {
return 'Edit ' + (this.form ? this.form.title : 'Your Form') return 'Edit ' + (this.form ? this.form.title : 'Your Form')
}, }
}, },
watch: { watch: {
@ -92,7 +92,7 @@ export default {
}, },
created () {}, created () {},
destroyed () {}, unmounted () {},
mounted () { mounted () {
window.onbeforeunload = () => { window.onbeforeunload = () => {

View File

@ -1,23 +1,27 @@
<template> <template>
<div> <div>
<h3 class="font-semibold text-2xl text-gray-900">Password</h3> <h3 class="font-semibold text-2xl text-gray-900">
Password
</h3>
<small class="text-gray-600">Manage your password.</small> <small class="text-gray-600">Manage your password.</small>
<form @submit.prevent="update" @keydown="form.onKeydown($event)" class="mt-3"> <form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" :message="$t('password_updated')" /> <alert-success class="mb-5" :form="form" message="Password updated." />
<!-- Password --> <!-- Password -->
<text-input native-type="password" <text-input native-type="password"
name="password" :form="form" :label="$t('password')" :required="true" name="password" :form="form" label="Password" :required="true"
/> />
<!-- Password Confirmation--> <!-- Password Confirmation-->
<text-input native-type="password" <text-input native-type="password"
name="password_confirmation" :form="form" :label="$t('confirm_password')" :required="true" name="password_confirmation" :form="form" label="Confirm Password" :required="true"
/> />
<!-- Submit Button --> <!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4">Update password</v-button> <v-button :loading="form.busy" class="mt-4">
Update password
</v-button>
</form> </form>
</div> </div>
</template> </template>
@ -27,8 +31,8 @@ import Form from 'vform'
import SeoMeta from '../../mixins/seo-meta.js' import SeoMeta from '../../mixins/seo-meta.js'
export default { export default {
scrollToTop: false,
mixins: [SeoMeta], mixins: [SeoMeta],
scrollToTop: false,
data: () => ({ data: () => ({
metaTitle: 'Password', metaTitle: 'Password',

View File

@ -1,19 +1,23 @@
<template> <template>
<div> <div>
<h3 class="font-semibold text-2xl text-gray-900">Profile details</h3> <h3 class="font-semibold text-2xl text-gray-900">
Profile details
</h3>
<small class="text-gray-600">Update your username and manage your account details.</small> <small class="text-gray-600">Update your username and manage your account details.</small>
<form @submit.prevent="update" @keydown="form.onKeydown($event)" class="mt-3"> <form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" :message="$t('info_updated')" /> <alert-success class="mb-5" :form="form" message="Your info has been updated!" />
<!-- Name --> <!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" :required="true" /> <text-input name="name" :form="form" label="Name" :required="true" />
<!-- Email --> <!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" /> <text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button --> <!-- Submit Button -->
<v-button :loading="form.busy" class="mt-4">Save changes</v-button> <v-button :loading="form.busy" class="mt-4">
Save changes
</v-button>
</form> </form>
</div> </div>
</template> </template>
@ -24,8 +28,8 @@ import { mapGetters } from 'vuex'
import SeoMeta from '../../mixins/seo-meta.js' import SeoMeta from '../../mixins/seo-meta.js'
export default { export default {
scrollToTop: false,
mixins: [SeoMeta], mixins: [SeoMeta],
scrollToTop: false,
data: () => ({ data: () => ({
metaTitle: 'Profile', metaTitle: 'Profile',

View File

@ -22,9 +22,9 @@ import SeoMeta from '../../mixins/seo-meta.js'
export default { export default {
components: { OpenFormFooter }, components: { OpenFormFooter },
mixins: [SeoMeta],
layout: 'default', layout: 'default',
middleware: 'auth', middleware: 'auth',
mixins: [SeoMeta],
data: () => ({ data: () => ({
metaTitle: 'Subscription Success', metaTitle: 'Subscription Success',
@ -36,7 +36,7 @@ export default {
this.interval = setInterval(() => this.checkSubscription(), 5000) this.interval = setInterval(() => this.checkSubscription(), 5000)
}, },
beforeDestroy () { beforeUnmount () {
clearInterval(this.interval) clearInterval(this.interval)
}, },

View File

@ -28,7 +28,7 @@ function hookLogEvent (binding) {
// Register directive to log event // Register directive to log event
const registeredListeners = {} const registeredListeners = {}
Vue.directive('track', { Vue.directive('track', {
bind (el, binding, vnode) { beforeMount (el, binding, vnode) {
registeredListeners[el] = () => { registeredListeners[el] = () => {
hookLogEvent(binding) hookLogEvent(binding)
} }

View File

@ -1,7 +1,6 @@
import axios from 'axios' import axios from 'axios'
import store from '~/store' import store from '~/store'
import router from '~/router' import router from '~/router'
import i18n from '~/plugins/i18n'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
function addPasswordToFormRequest (request) { function addPasswordToFormRequest (request) {

View File

@ -1,33 +0,0 @@
import Vue from 'vue'
import store from '~/store'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n({
locale: 'en',
messages: {}
})
/**
* @param {String} locale
*/
export async function loadMessages (locale) {
if (Object.keys(i18n.getLocaleMessage(locale)).length === 0) {
if (locale) {
const langFiles = import.meta.glob('../lang/**.json', {eager: true})
const messages = langFiles[`../lang/${locale}.json`]
i18n.setLocaleMessage(locale, messages)
}
}
if (i18n.locale !== locale) {
i18n.locale = locale
}
}
;(async function () {
await loadMessages(store.getters['lang/locale'])
})()
export default i18n

View File

@ -2,10 +2,11 @@ import routes from './routes'
import { createWebHistory, createRouter } from 'vue-router' import { createWebHistory, createRouter } from 'vue-router'
import * as Sentry from '@sentry/vue' import * as Sentry from '@sentry/vue'
import store from '../store' import store from '../store'
import { defineComponent } from 'vue'
// import { nextTick } from '@vue/compat' // import { nextTick } from '@vue/compat'
// The middleware for every page of the application. // The middleware for every page of the application.
const globalMiddleware = ['locale', 'check-auth', 'notion-connection'] const globalMiddleware = ['check-auth', 'notion-connection']
// Load middleware modules dynamically. // Load middleware modules dynamically.
const requireContext = import.meta.glob('../middleware/**/*.js', { eager: true }) const requireContext = import.meta.glob('../middleware/**/*.js', { eager: true })
@ -30,6 +31,13 @@ function createCustomRouter () {
return router return router
} }
async function getMatchedComponents (to) {
return resolveComponents(to.matched.map((record) => {
const component = record.components.default
return typeof component === 'function' ? defineComponent(component) : component
}))
}
/** /**
* Global router guard. * Global router guard.
* *
@ -54,9 +62,7 @@ async function beforeEach (to, from, next) {
try { try {
// Get the matched components and resolve them. // Get the matched components and resolve them.
components = await resolveComponents( components = await getMatchedComponents(to)
router.getMatchedComponents({ ...to })
)
} catch (error) { } catch (error) {
if (/^Loading( CSS)? chunk (\d)+ failed\./.test(error.message)) { if (/^Loading( CSS)? chunk (\d)+ failed\./.test(error.message)) {
window.location.reload(true) window.location.reload(true)
@ -78,15 +84,14 @@ async function beforeEach (to, from, next) {
// Call each middleware. // Call each middleware.
callMiddleware(middleware, to, from, (...args) => { callMiddleware(middleware, to, from, (...args) => {
console.log('in', store)
// Set the application layout only if "next()" was called with no args. // Set the application layout only if "next()" was called with no args.
if (args.length === 0) { if (args.length === 0) {
if (components[0].layout) { if (components[0].layout) {
router.app.setLayout(components[0].layout) store.commit('app/setLayout', components[0].layout)
} else if (components[0].default && components[0].default.layout) { } else if (components[0].default && components[0].default.layout) {
router.app.setLayout(components[0].default.layout) store.commit('app/setLayout', components[0].default.layout)
} else { } else {
router.app.setLayout(null) store.commit('app/setLayout', null)
} }
} }
@ -218,7 +223,7 @@ function scrollBehavior (to, from, savedPosition) {
return { selector: to.hash } return { selector: to.hash }
} }
const [component] = router.getMatchedComponents({ ...to }).slice(-1) const [component] = getMatchedComponents(to)
if (component && component.scrollToTop === false) { if (component && component.scrollToTop === false) {
return {} return {}
@ -226,7 +231,7 @@ function scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
resolve({ x: 0, y: 0 }) resolve({ left: 0, top: 0 })
}, 190) }, 190)
}) })
} }

View File

@ -1,49 +0,0 @@
import Cookies from 'js-cookie'
import * as types from '../mutation-types'
const { locale, locales } = window.config
// state
export const state = {
locale: getLocale(locales, locale),
locales: locales
}
// getters
export const getters = {
locale: state => state.locale,
locales: state => state.locales
}
// mutations
export const mutations = {
[types.SET_LOCALE] (state, { locale }) {
state.locale = locale
}
}
// actions
export const actions = {
setLocale ({ commit }, { locale }) {
commit(types.SET_LOCALE, { locale })
Cookies.set('locale', locale, { expires: 365 })
}
}
/**
* @param {String[]} locales
* @param {String} fallback
* @return {String}
*/
function getLocale (locales, fallback) {
const locale = Cookies.get('locale')
if (Object.prototype.hasOwnProperty.call(locales, locale)) {
return locale
} else if (locale) {
Cookies.remove('locale')
}
return fallback
}