Initial commit
This commit is contained in:
54
resources/js/pages/settings/account.vue
Normal file
54
resources/js/pages/settings/account.vue
Normal 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>
|
||||
84
resources/js/pages/settings/admin.vue
Normal file
84
resources/js/pages/settings/admin.vue
Normal 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>
|
||||
43
resources/js/pages/settings/billing.vue
Normal file
43
resources/js/pages/settings/billing.vue
Normal 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>
|
||||
123
resources/js/pages/settings/index.vue
Normal file
123
resources/js/pages/settings/index.vue
Normal 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>
|
||||
49
resources/js/pages/settings/password.vue
Normal file
49
resources/js/pages/settings/password.vue
Normal 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>
|
||||
57
resources/js/pages/settings/profile.vue
Normal file
57
resources/js/pages/settings/profile.vue
Normal 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>
|
||||
126
resources/js/pages/settings/workspace.vue
Normal file
126
resources/js/pages/settings/workspace.vue
Normal 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>
|
||||
Reference in New Issue
Block a user