Initial commit

This commit is contained in:
Julien Nahum
2022-09-20 21:59:52 +02:00
commit f8e6cd4dd6
479 changed files with 77078 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
<template>
<card title="Account" class="bg-gray-50 dark:bg-notion-dark-light">
<h3 class="text-lg font-semibold mb-4">
Your Account
</h3>
<p class="text-gray-800 dark:text-gray-200">
You can delete your account. All your data will be removed. <span class="font-semibold">This cannot be undone.</span>
</p>
<!-- Submit Button -->
<v-button :loading="loading" class="mt-4" color="red" @click="alertConfirm('Do you really want to delete your account?',deleteAccount)">
Delete my account
</v-button>
</card>
</template>
<script>
import Form from 'vform'
import axios from 'axios'
export default {
scrollToTop: false,
metaInfo () {
return { title: 'Account' }
},
data: () => ({
form: new Form({
identifier: ''
}),
loading: false
}),
methods: {
async deleteAccount () {
this.loading = true
axios.delete('/api/user').then(async (response) => {
this.loading = false
this.alertSuccess(response.data.message)
// Log out the user.
await this.$store.dispatch('auth/logout')
// Redirect to login.
this.$router.push({ name: 'login' })
}).catch((error) => {
this.alertError(error.response.data.message)
this.loading = false
})
}
}
}
</script>

View File

@@ -0,0 +1,84 @@
<template>
<card title="Admin" class="bg-gray-50 dark:bg-notion-dark-light">
<h3 class="text-lg font-semibold mb-4">
Tools
</h3>
<div class="flex flex-wrap mb-10">
<a href="/stats">
<v-button class="mx-1" color="gray" shade="lighter">
Stats
</v-button>
</a>
<a href="/horizon">
<v-button class="mx-1" color="gray" shade="lighter">
Horizon
</v-button>
</a>
</div>
<h3 class="text-lg font-semibold mb-4">
Impersonate User
</h3>
<form @submit.prevent="impersonate" @keydown="form.onKeydown($event)">
<!-- Password -->
<text-input name="identifier" :form="form" label="Identifier"
:required="true" help="User Id, User Email or Form Slug"
/>
<!-- Submit Button -->
<v-button :loading="loading" type="success" color="nt-blue" class="mt-4 w-full">
Impersonate User
</v-button>
</form>
</card>
</template>
<script>
import Form from 'vform'
import axios from 'axios'
import FancyLink from '../../components/common/FancyLink'
export default {
components: { FancyLink },
middleware: 'admin',
scrollToTop: false,
metaInfo () {
return { title: 'Admin' }
},
data: () => ({
form: new Form({
identifier: ''
}),
loading: false
}),
methods: {
async impersonate () {
this.loading = true
this.$store.commit('auth/startImpersonating')
axios.get('/api/admin/impersonate/' + encodeURI(this.form.identifier)).then(async (response) => {
this.loading = false
// Save the token.
this.$store.dispatch('auth/saveToken', {
token: response.data.token,
remember: false
})
// Fetch the user.
await this.$store.dispatch('auth/fetchUser')
// Redirect to the dashboard.
this.$store.commit('open/workspaces/set', [])
this.$router.push({ name: 'home' })
}).catch((error) => {
this.alertError(error.response.data.message)
this.loading = false
})
// this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<card title="Billing" class="bg-gray-50 dark:bg-notion-dark-light">
<v-button color="gray" shade="light" :loading="billingLoading" @click.prevent="openBillingDashboard">
Manage Subscription
</v-button>
<v-button color="red" class="mt-3" @click.prevent="cancelSubscription">
Cancel Subscription
</v-button>
</card>
</template>
<script>
import axios from 'axios'
import VButton from '../../components/common/Button'
export default {
components: { VButton },
scrollToTop: false,
metaInfo () {
return { title: 'Billing' }
},
data: () => ({
billingLoading: false
}),
methods: {
cancelSubscription () {
// this.alertError('Sorry to see you leave 😢')
},
openBillingDashboard () {
this.billingLoading = true
axios.get('/api/subscription/billing-portal').then((response) => {
const url = response.data.portal_url
window.location = url
}).finally(() => {
this.billingLoading = false
})
}
}
}
</script>

View File

@@ -0,0 +1,123 @@
<template>
<div class="flex flex-wrap mt-6 md:max-w-3xl w-full md:mx-auto">
<div class="w-full md:w-1/3 md:pr-4">
<card :padding="false" class="bg-gray-50 dark:bg-notion-dark-light">
<ul>
<li v-for="tab in tabs" :key="tab.route">
<router-link :to="{ name: tab.route }"
class="px-6 py-4 flex items-center text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 hover:text-gray-900 hover:bg-gray-50 dark:hover:bg-gray-900 rounded"
active-class="text-nt-blue bg-indigo-50 dark:bg-gray-800 hover:bg-blue-50"
>
<template v-if="tab.route == 'settings.profile'">
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</template>
<template v-else-if="tab.route == 'settings.account'">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7a4 4 0 11-8 0 4 4 0 018 0zM9 14a6 6 0 00-6 6v1h12v-1a6 6 0 00-6-6zM21 12h-6" />
</svg>
</template>
<template v-else-if="tab.route == 'settings.workspaces'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</template>
<template v-else-if="tab.route == 'settings.billing'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"
/>
</svg>
</template>
<template v-else-if="tab.route == 'settings.password'">
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
</template>
<span class="ml-2">
{{ tab.name }}
</span>
</router-link>
</li>
<li v-if="user.admin">
<router-link :to="{ name: 'settings.admin' }"
class="px-6 py-4 flex items-center text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 hover:text-gray-900 hover:bg-gray-50 dark:hover:bg-gray-900 rounded"
active-class="text-nt-blue bg-indigo-50 dark:bg-gray-800 hover:bg-blue-50"
>
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<span class="ml-2">
Admin
</span>
</router-link>
</li>
</ul>
</card>
</div>
<div class="w-full md:w-2/3">
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
computed: {
tabs () {
const tabs = [
{
name: 'Workspaces',
route: 'settings.workspaces'
},
{
name: this.$t('profile'),
route: 'settings.profile'
},
{
name: this.$t('password'),
route: 'settings.password'
},
{
name: 'Delete Account',
route: 'settings.account'
}
]
if (this.user.is_subscribed) {
tabs.splice(1, 0, {
name: 'Billing',
route: 'settings.billing'
})
}
return tabs
},
...mapGetters({
user: 'auth/user'
})
}
}
</script>

View File

@@ -0,0 +1,49 @@
<template>
<card :title="$t('your_password')" class="bg-gray-50 dark:bg-notion-dark-light">
<form @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" :message="$t('password_updated')" />
<!-- Password -->
<text-input class="mt-8" native-type="password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Password Confirmation-->
<text-input class="mt-8" native-type="password"
name="password_confirmation" :form="form" :label="$t('confirm_password')" :required="true"
/>
<!-- Submit Button -->
<v-button :loading="form.busy" type="success" color="nt-blue" class="mt-4 w-full">
{{ $t('update') }}
</v-button>
</form>
</card>
</template>
<script>
import Form from 'vform'
export default {
scrollToTop: false,
metaInfo () {
return { title: this.$t('settings') }
},
data: () => ({
form: new Form({
password: '',
password_confirmation: ''
})
}),
methods: {
async update () {
await this.form.patch('/api/settings/password')
this.form.reset()
}
}
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<card title="Profile" class="bg-gray-50 dark:bg-notion-dark-light">
<form @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" :message="$t('info_updated')" />
<!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" :required="true" />
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" />
<!-- Submit Button -->
<v-button :loading="form.busy" type="success" color="nt-blue" class="mt-4 w-full">
{{ $t('update') }}
</v-button>
</form>
</card>
</template>
<script>
import Form from 'vform'
import { mapGetters } from 'vuex'
export default {
scrollToTop: false,
metaInfo () {
return { title: this.$t('settings') }
},
data: () => ({
form: new Form({
name: '',
email: ''
})
}),
computed: mapGetters({
user: 'auth/user'
}),
created () {
// Fill the form with user data.
this.form.keys().forEach(key => {
this.form[key] = this.user[key]
})
},
methods: {
async update () {
const { data } = await this.form.patch('/api/settings/profile')
this.$store.dispatch('auth/updateUser', { user: data })
}
}
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<card title="Workspaces" class="bg-gray-50 dark:bg-notion-dark-light">
<div v-if="loading" class="w-full text-blue-500 text-center">
<loader class="h-10 w-10 p-5" />
</div>
<div v-else>
<div v-for="workspace in workspaces" :key="workspace.id"
class="border border-nt-blue-light shadow rounded-md p-4 mb-5 max-w-sm w-full flex group mx-auto bg-white dark:bg-notion-dark items-center"
>
<div class="flex space-x-4 flex-grow cursor-pointer" role="button" @click.prevent="switchWorkspace(workspace)">
<img v-if="isUrl(workspace.icon)" :src="workspace.icon" :alt="workspace.name + ' icon'"
class="rounded-full h-12 w-12"
>
<div v-else class="rounded-full bg-nt-blue-lighter h-12 w-12 text-2xl pt-2 text-center overflow-hidden"
v-text="workspace.icon"
/>
<div class="flex-1 flex items-center space-y-4 py-1">
<p class="font-bold truncate" v-text="workspace.name" />
</div>
</div>
<div v-if="workspaces.length > 1" class="block md:hidden group-hover:block text-red-500 p-2 rounded hover:bg-red-50" role="button" @click="deleteWorkspace(workspace)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</div>
</div>
<div class="max-w-sm w-full mx-auto mt-4">
<v-button :loading="loading" class="w-full" @click="workspaceModal=true">
Create a new workspace
</v-button>
</div>
</div>
<!-- Workspace modal -->
<modal :show="workspaceModal" @close="workspaceModal=false">
<div class="px-4">
<form @submit.prevent="createWorkspace" @keydown="form.onKeydown($event)">
<h2 class="text-2xl font-bold z-10 truncate mb-5 text-nt-blue">
Create Workspace
</h2>
<div>
<text-input name="name" class="mt-4" :form="form" :required="true"
label="Workspace Name"
/>
<text-input name="emoji" class="mt-4" :form="form" :required="false"
label="Emoji"
/>
</div>
<div class="flex justify-end mt-4">
<v-button :loading="form.busy" class="mr-1">Save</v-button>
<v-button color="gray" shade="light" @click.prevent="workspaceModal=false">Close</v-button>
</div>
</form>
</div>
</modal>
</card>
</template>
<script>
import Form from 'vform'
import { mapActions, mapState } from 'vuex'
export default {
components: { },
scrollToTop: false,
metaInfo () {
return { title: 'Workspaces' }
},
data: () => ({
form: new Form({
name: '',
emoji: ''
}),
workspaceModal: false
}),
mounted () {
this.loadWorkspaces()
},
computed: {
...mapState({
workspaces: state => state['open/workspaces'].content,
loading: state => state['open/workspaces'].loading
})
},
methods: {
...mapActions({
loadWorkspaces: 'open/workspaces/loadIfEmpty'
}),
switchWorkspace (workspace) {
this.$store.commit('open/workspaces/setCurrentId', workspace.id)
this.$router.push({ name: 'home' })
this.$store.dispatch('open/forms/load', workspace.id)
},
deleteWorkspace (workspace) {
this.alertConfirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
this.$store.dispatch('open/workspaces/delete', workspace.id).then(() => {
this.alertSuccess('Workspace successfully removed.')
})
})
},
isUrl (str) {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
return !!pattern.test(str)
},
async createWorkspace () {
const { data } = await this.form.post('/api/open/workspaces/create')
this.$store.dispatch('open/workspaces/load')
this.workspaceModal = false
}
}
}
</script>