create form as a guest (#29)

* create form as a guest

* Remove vue-tour

* Refactor initForm function and improve template merge

* Add templates to navbar

* Fix template preview submit issue

Co-authored-by: Chirag Chhatrala <chirag@notionforms.io>
This commit is contained in:
Julien Nahum 2022-11-16 11:56:49 +01:00 committed by GitHub
parent 0ce0a1dcc1
commit b9ba6e1e29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1951 additions and 1393 deletions

2349
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,6 @@
"vue-prism-editor": "^1.2.2",
"vue-router": "^3.5.2",
"vue-tailwind": "^2.5.0",
"vue-tour": "^2.0.0",
"vue2-editor": "^2.10.3",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",

6
resources/js/app.js vendored
View File

@ -6,8 +6,6 @@ import App from '~/components/App'
import LoadScript from 'vue-plugin-load-script'
import Base from './base'
import VueTour from 'vue-tour'
import '~/plugins'
import '~/components'
@ -16,10 +14,6 @@ Vue.config.productionTip = false
Vue.mixin(Base)
Vue.use(LoadScript)
/* Vue Tour */
require('vue-tour/dist/vue-tour.css')
Vue.use(VueTour)
/* eslint-disable no-new */
new Vue({
i18n,

View File

@ -15,10 +15,10 @@
<workspace-dropdown class="ml-6"/>
</div>
<div class="hidden md:block ml-auto relative">
<!-- <router-link :to="{name:'integrations'}"-->
<!-- class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">-->
<!-- Integrations-->
<!-- </router-link>-->
<router-link :to="{name:'templates'}"
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8">
Templates
</router-link>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"
@click.prevent="$getCrisp().push(['do', 'helpdesk:search'])" v-if="hasCrisp"
>
@ -99,7 +99,7 @@
{{ $t('login') }}
</router-link>
<v-button size="small" :to="{ name: 'register' }" color="outline-blue" v-track.nav_create_form_click :arrow="true">
<v-button size="small" :to="{ name: 'forms.create.guest' }" color="outline-blue" v-track.nav_create_form_click :arrow="true">
Create a form
</v-button>

View File

@ -7,17 +7,17 @@
</div>
<modal :show="showPremiumModal" @close="showPremiumModal=false">
<h2 class="text-nt-blue">
OpenForm PRO
OpnForm PRO
</h2>
<h4 v-if="user.is_subscribed && !user.has_enterprise_subscription" class="text-center mt-5">
We're happy to have you as a Pro customer. If you're having any issue with OpenForm, or if you have a
We're happy to have you as a Pro customer. If you're having any issue with OpnForm, or if you have a
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
<br><br>
If you need to collaborate, or to work with multiple workspaces, or just larger file uploads, you can
also upgrade our subscription to get an Enterprise subscription.
</h4>
<h4 v-if="user.is_subscribed && user.has_enterprise_subscription" class="text-center mt-5">
We're happy to have you as an Enterprise customer. If you're having any issue with OpenForm, or if you have a
We're happy to have you as an Enterprise customer. If you're having any issue with OpnForm, or if you have a
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
</h4>
<p v-if="!user.is_subscribed" class="mt-4">
@ -25,7 +25,7 @@
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
>
PRO
</span> tag are available in the Pro plan of OpenForm. <b>You can play around and try all Pro features
</span> tag are available in the Pro plan of OpnForm. <b>You can play around and try all Pro features
within
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
to

View File

@ -91,7 +91,7 @@
>
<template #submit-btn="{submitForm}">
<open-form-button :loading="loading" :theme="theme" :color="form.color" class="mt-2 px-8 mx-1"
@click="submitForm"
@click.prevent="submitForm"
>
{{ form.submit_button_text }}
</open-form-button>

View File

@ -1,14 +1,13 @@
<template>
<div v-if="form" id="form-editor" class="w-full flex flex-grow relative overflow-x-hidden">
<!-- Form fields selection -->
<v-tour name="tutorial" :steps="steps"/>
<div class="w-full md:w-1/2 lg:w-2/5 border-r relative overflow-y-scroll md:max-w-sm flex-shrink-0">
<div class="p-4 bg-blue-50 border-b text-nt-blue-dark md:hidden">
We suggest you create this form on a device with a larger screen such as computed. That will allow you
to preview your form changes.
</div>
<div class="p-4 pb-0">
<a href="#" @click.prevent="$router.back()" class="flex text-blue mb-2 font-semibold text-sm">
<a v-if="!isGuest" href="#" @click.prevent="$router.back()" class="flex text-blue mb-2 font-semibold text-sm">
<svg class="w-3 h-3 text-blue mt-1 mr-1" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round"/>
@ -88,6 +87,11 @@ export default {
type: Boolean,
default: false
},
isGuest: {
required: false,
type: Boolean,
default: false
},
},
data() {
@ -123,7 +127,7 @@ export default {
{
target: '#v-step-0',
header: {
title: 'Welcome to the OpenForm Editor!'
title: 'Welcome to the OpnForm Editor!'
},
content: 'Discover <strong>your form Editor</strong>!'
},
@ -160,20 +164,16 @@ export default {
mounted() {
this.$emit('mounted')
this.startTour()
},
methods: {
startTour() {
if (!this.user.has_forms) {
this.$tours.tutorial.start()
}
},
showValidationErrors() {
this.showFormErrorModal = true
},
saveForm() {
if (this.isEdit) {
if(this.isGuest) {
this.saveFormGuest()
} else if (this.isEdit) {
this.saveFormEdit()
} else {
this.saveFormCreate()
@ -230,6 +230,9 @@ export default {
}).finally(() => {
this.updateFormLoading = false
})
},
saveFormGuest() {
this.$emit('openRegister')
}
}
}

View File

@ -266,7 +266,7 @@ export default {
];
},
init() {
if (this.$route.name === 'forms.create') { // Set Default fields
if (this.$route.name === 'forms.create' || this.$route.name === 'forms.create.guest') { // Set Default fields
this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields()
} else {
this.formFields = clonedeep(this.form.properties).map((field) => {

View File

@ -47,6 +47,17 @@
</svg>
Duplicate form
</a>
<a href="#" v-if="user.admin"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="showCreateTemplateModal=true"
>
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"/>
</svg>
Create Template
</a>
<a href="#"
class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center"
v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
@ -60,17 +71,6 @@
</svg>
Delete form
</a>
<a href="#" v-if="user.admin"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="showCreateTemplateModal=true"
>
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"/>
</svg>
Create Template
</a>
</dropdown>
<create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/>
@ -127,4 +127,3 @@ export default {
}
}
</script>

View File

@ -0,0 +1,50 @@
import Form from "vform";
export default {
methods: {
initForm() {
this.form = new Form({
title: 'My Form',
description: null,
visibility: 'public',
workspace_id: this.workspace?.id,
properties: [],
notifies: false,
slack_notifies: false,
send_submission_confirmation: false,
webhook_url: null,
// Customization
theme: 'default',
width: 'centered',
dark_mode: 'auto',
color: '#3B82F6',
hide_title: false,
no_branding: false,
uppercase_labels: true,
transparent_background: false,
closes_at: null,
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
// Submission
submit_button_text: 'Submit',
re_fillable: false,
re_fill_button_text: 'Fill Again',
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
notification_sender: 'OpnForm',
notification_subject: 'We saved your answers',
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
notifications_include_submission: true,
use_captcha: false,
is_rating: false,
rating_max_value: 5,
max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
// Security & Privacy
can_be_indexed: true
})
},
}
}

View File

@ -0,0 +1,105 @@
<template>
<div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<form @submit.prevent="login" @keydown="form.onKeydown($event)" class="mt-4">
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<!-- Password -->
<text-input native-type="password" placeholder="Your password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Remember Me -->
<div class="relative flex items-center my-5">
<v-checkbox v-model="remember" class="w-full md:w-1/2" name="remember" size="small">
{{ $t('remember_me') }}
</v-checkbox>
<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">
Forgot your password?
</a>
</div>
</div>
<!-- Submit Button -->
<v-button dusk="btn_login" :loading="form.busy">Log in to continue</v-button>
<p class="text-gray-500 mt-4">
Don't have an account?
<a href="#" v-if="isQuick" @click.prevent="$emit('openRegister')" class="font-semibold ml-1">Sign Up</a>
<router-link v-else :to="{name:'register'}" class="font-semibold ml-1">Sign Up</router-link>
</p>
</form>
</div>
</template>
<script>
import Form from 'vform'
import Cookies from 'js-cookie'
import OpenFormFooter from '../../../components/pages/OpenFormFooter'
import Testimonials from '../../../components/pages/welcome/Testimonials'
import ForgotPasswordModal from '../ForgotPasswordModal'
export default {
name: 'LoginForm',
components: {
OpenFormFooter,
Testimonials,
ForgotPasswordModal
},
props: {
isQuick: {
type: Boolean,
required: false,
default: false
}
},
data: () => ({
form: new Form({
email: '',
password: ''
}),
remember: false,
showForgotModal: false
}),
methods: {
async login () {
// Submit the form.
const { data } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', {
token: data.token,
remember: this.remember
})
// Fetch the user.
await this.$store.dispatch('auth/fetchUser')
// Redirect home.
this.redirect()
},
redirect () {
if(this.isQuick){
this.$emit('afterQuickLogin')
return
}
const intendedUrl = Cookies.get('intended_url')
if (intendedUrl) {
Cookies.remove('intended_url')
this.$router.push({ path: intendedUrl })
} else {
this.$router.push({ name: 'home' })
}
}
}
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<div>
<!-- Login modal -->
<modal :show="showLoginModal" @close="showLoginModal=false" max-width="lg">
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
<template #title>
Login to OpnForm
</template>
<div class="px-4">
<login-form :isQuick="true" @openRegister="openRegister" @afterQuickLogin="afterQuickLogin" />
</div>
</modal>
<!-- Register modal -->
<modal :show="showRegisterModal" @close="$emit('close')" max-width="lg">
<template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</template>
<template #title>
Create an account
</template>
<div class="px-4">
<register-form :isQuick="true" @openLogin="openLogin" @afterQuickLogin="afterQuickLogin" />
</div>
</modal>
</div>
</template>
<script>
import LoginForm from './LoginForm'
import RegisterForm from './RegisterForm'
export default {
name: 'QuickRegister',
components: {
LoginForm,
RegisterForm
},
props: {
showRegisterModal: {
type: Boolean,
required: true
}
},
data: () => ({
showLoginModal: false,
}),
mounted() {
},
methods: {
openLogin(){
this.showLoginModal = true
this.$emit('close')
},
openRegister(){
this.showLoginModal = false
this.$emit('reopen')
},
afterQuickLogin(){
this.showLoginModal = false
this.$emit('afterLogin')
}
}
}
</script>

View File

@ -0,0 +1,128 @@
<template>
<div>
<form @submit.prevent="register" @keydown="form.onKeydown($event)" class="mt-4">
<!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" placeholder="Your name" :required="true" />
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option"
label="How did you hear about us?" :required="true"
/>
<!-- Password -->
<text-input native-type="password" placeholder="Enter password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password"
name="password_confirmation" :label="$t('confirm_password')"
/>
<checkbox-input :form="form" name="agree_terms" :required="true">
<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.
</template>
</checkbox-input>
<!-- Submit Button -->
<v-button :loading="form.busy">Create an account</v-button>
<p class="text-gray-500 mt-4">
Already have an account?
<a href="#" v-if="isQuick" @click.prevent="$emit('openLogin')" class="font-semibold ml-1">Log In</a>
<router-link v-else :to="{name:'login'}" class="font-semibold ml-1">Log In</router-link>
</p>
<!-- GitHub Register Button -->
<login-with-github />
</form>
</div>
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
import SelectInput from '../../../components/forms/SelectInput'
import { initCrisp } from '../../../middleware/check-auth'
export default {
name: 'RegisterForm',
components: {
SelectInput,
LoginWithGithub,
},
props: {
isQuick: {
type: Boolean,
required: false,
default: false
}
},
data: () => ({
form: new Form({
name: '',
email: '',
password: '',
password_confirmation: '',
agree_terms: false
}),
mustVerifyEmail: false
}),
computed: {
hearAboutUsOptions () {
const options = [
{ name: 'Facebook', value: 'facebook' },
{ name: 'Twitter', value: 'twitter' },
{ name: 'Reddit', value: 'reddit' },
{ name: 'Github', value: 'github' },
{ name: 'Search Engine (Google, DuckDuckGo...)', value: 'search_engine' },
{ name: 'Friend or Colleague', value: 'friend_colleague' },
{ name: 'Blog/Article', value: 'blog_article' }
].map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
options.push({ name: 'Other', value: 'other' })
return options
}
},
methods: {
async register () {
// Register the user.
const { data } = await this.form.post('/api/register')
// Must verify email fist.
if (data.status) {
this.mustVerifyEmail = true
} else {
// Log in the user.
const { data: { token } } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', { token })
// Update the user.
await this.$store.dispatch('auth/updateUser', { user: data })
// Track event
this.$logEvent('register', { source: this.form.hear_about_us })
initCrisp(data).then(() => {
this.$getCrisp().push(['set', 'session:event', [[['register', {}, 'blue']]]])
})
// Redirect
if(this.isQuick){
this.$emit('afterQuickLogin')
}else{
this.$router.push({ name: 'forms.create' })
}
}
}
}
}
</script>

View File

@ -1,7 +1,5 @@
<template>
<div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<div class="flex mt-6 mb-10">
<div class="w-full md:max-w-6xl mx-auto px-4 flex md:flex-row-reverse flex-wrap">
<div class="w-full md:w-1/2 md:p-6">
@ -11,35 +9,7 @@
</h2>
<small>Welcome back! Please enter your details.</small>
<form @submit.prevent="login" @keydown="form.onKeydown($event)" class="mt-4">
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<!-- Password -->
<text-input native-type="password" placeholder="Your password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Remember Me -->
<div class="relative flex items-center my-5">
<v-checkbox v-model="remember" class="w-full md:w-1/2" name="remember" size="small">
{{ $t('remember_me') }}
</v-checkbox>
<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">
Forgot your password?
</a>
</div>
</div>
<!-- Submit Button -->
<v-button dusk="btn_login" :loading="form.busy">Log in to continue</v-button>
<p class="text-gray-500 mt-4">
Don't have an account? <router-link :to="{name:'register'}" class="font-semibold ml-1">Sign Up</router-link>
</p>
</form>
<login-form />
</div>
</div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 ">
@ -86,17 +56,15 @@
</template>
<script>
import Form from 'vform'
import Cookies from 'js-cookie'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import Testimonials from '../../components/pages/welcome/Testimonials'
import ForgotPasswordModal from './ForgotPasswordModal'
import LoginForm from './components/LoginForm'
export default {
components: {
OpenFormFooter,
Testimonials,
ForgotPasswordModal
LoginForm
},
middleware: 'guest',
@ -106,42 +74,11 @@ export default {
},
data: () => ({
form: new Form({
email: '',
password: ''
}),
remember: false,
showForgotModal: false
}),
methods: {
async login () {
// Submit the form.
const { data } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', {
token: data.token,
remember: this.remember
})
// Fetch the user.
await this.$store.dispatch('auth/fetchUser')
// Redirect home.
this.redirect()
},
redirect () {
const intendedUrl = Cookies.get('intended_url')
if (intendedUrl) {
Cookies.remove('intended_url')
this.$router.push({ path: intendedUrl })
} else {
this.$router.push({ name: 'home' })
}
}
}
}
</script>

View File

@ -9,43 +9,7 @@
</h2>
<small>Sign up in less than 2 minutes.</small>
<form @submit.prevent="register" @keydown="form.onKeydown($event)" class="mt-4">
<!-- Name -->
<text-input name="name" :form="form" :label="$t('name')" placeholder="Your name" :required="true" />
<!-- Email -->
<text-input name="email" :form="form" :label="$t('email')" :required="true" placeholder="Your email address" />
<select-input name="hear_about_us" :options="hearAboutUsOptions" :form="form" placeholder="Select option"
label="How did you hear about us?" :required="true"
/>
<!-- Password -->
<text-input native-type="password" placeholder="Enter password"
name="password" :form="form" :label="$t('password')" :required="true"
/>
<!-- Password Confirmation-->
<text-input native-type="password" :form="form" :required="true" placeholder="Enter confirm password"
name="password_confirmation" :label="$t('confirm_password')"
/>
<checkbox-input :form="form" name="agree_terms" :required="true">
<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.
</template>
</checkbox-input>
<!-- Submit Button -->
<v-button :loading="form.busy">Create an account</v-button>
<p class="text-gray-500 mt-4">
Already have an account? <router-link :to="{name:'login'}" class="font-semibold ml-1">Log In</router-link>
</p>
<!-- GitHub Register Button -->
<login-with-github />
</form>
<register-form />
</div>
</div>
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0 ">
@ -92,19 +56,15 @@
</template>
<script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
import SelectInput from '../../components/forms/SelectInput'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import { initCrisp } from '../../middleware/check-auth'
import Testimonials from '../../components/pages/welcome/Testimonials'
import RegisterForm from './components/RegisterForm'
export default {
components: {
Testimonials,
SelectInput,
LoginWithGithub,
OpenFormFooter
OpenFormFooter,
RegisterForm
},
middleware: 'guest',
@ -114,62 +74,13 @@ export default {
},
data: () => ({
form: new Form({
name: '',
email: '',
password: '',
password_confirmation: '',
agree_terms: false
}),
mustVerifyEmail: false
}),
computed: {
hearAboutUsOptions () {
const options = [
{ name: 'Facebook', value: 'facebook' },
{ name: 'Twitter', value: 'twitter' },
{ name: 'Reddit', value: 'reddit' },
{ name: 'Github', value: 'github' },
{ name: 'Search Engine (Google, DuckDuckGo...)', value: 'search_engine' },
{ name: 'Friend or Colleague', value: 'friend_colleague' },
{ name: 'Blog/Article', value: 'blog_article' }
].map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value)
options.push({ name: 'Other', value: 'other' })
return options
}
},
methods: {
async register () {
// Register the user.
const { data } = await this.form.post('/api/register')
// Must verify email fist.
if (data.status) {
this.mustVerifyEmail = true
} else {
// Log in the user.
const { data: { token } } = await this.form.post('/api/login')
// Save the token.
this.$store.dispatch('auth/saveToken', { token })
// Update the user.
await this.$store.dispatch('auth/updateUser', { user: data })
// Track event
this.$logEvent('register', { source: this.form.hear_about_us })
initCrisp(data).then(() => {
this.$getCrisp().push(['set', 'session:event', [[['register', {}, 'blue']]]])
})
// Redirect home.
this.$router.push({ name: 'forms.create' })
}
}
}
}
</script>

View File

@ -0,0 +1,153 @@
<template>
<div class="flex flex-wrap flex-col">
<transition v-if="stateReady" name="fade" mode="out-in">
<div key="2">
<form-editor v-if="!workspacesLoading" ref="editor"
class="w-full flex flex-grow"
:style="{
'max-height': editorMaxHeight + 'px'
}" :error="error"
:isGuest="isGuest"
@mounted="onResize"
@openRegister="openRegister"
/>
<div v-else class="text-center mt-4 py-6">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
</div>
</transition>
<quick-register :showRegisterModal="registerModal" @close="registerModal=false" @reopen="registerModal=true"
@afterLogin="afterLogin"/>
</div>
</template>
<script>
import store from '~/store'
import Form from 'vform'
import {mapState, mapActions} from 'vuex'
import QuickRegister from '../auth/components/QuickRegister'
import initForm from "../../mixins/form_editor/initForm"
const FormEditor = () => import('../../components/open/forms/components/FormEditor')
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
name: 'CreateFormGuest',
mixins: [initForm],
components: {
FormEditor,
QuickRegister
},
middleware: 'guest',
metaInfo() {
return {title: 'Create a new Form as Guest'}
},
beforeRouteEnter(to, from, next) {
loadTemplates()
next()
},
data() {
return {
stateReady: false,
loading: false,
error: '',
editorMaxHeight: 500,
registerModal: false,
isGuest: true
}
},
computed: {
...mapState({
workspaces: state => state['open/workspaces'].content,
workspacesLoading: state => state['open/workspaces'].loading,
}),
form: {
get() {
return this.$store.state['open/working_form'].content
},
/* We add a setter */
set(value) {
this.$store.commit('open/working_form/set', value)
}
},
workspace() {
return this.$store.getters['open/workspaces/getCurrent']()
},
},
watch: {
workspace() {
if (this.workspace) {
this.form.workspace_id = this.workspace.id
}
}
},
mounted() {
// Set as guest user
const guestWorkspace = {
id: null,
name: "Guest Workspace",
is_enterprise: false,
is_pro: false
}
this.$store.commit('open/workspaces/set', [guestWorkspace])
this.$store.commit('open/workspaces/setCurrentId', guestWorkspace.id)
this.initForm()
if (this.$route.query.template !== undefined && this.$route.query.template) {
const template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
if (template && template.structure) {
this.form = new Form({...this.form.data(), ...template.structure})
}
}
this.closeAlert()
this.stateReady = true
},
created() {
window.addEventListener('resize', this.onResize)
},
destroyed() {
window.removeEventListener('resize', this.onResize)
},
methods: {
...mapActions({
loadWorkspaces: 'open/workspaces/load'
}),
/**
* Compute max height of editor
*/
onResize() {
if (this.$refs.editor) {
this.editorMaxHeight = window.innerHeight - this.$refs.editor.$el.offsetTop
}
},
openRegister() {
this.registerModal = true
},
afterLogin() {
this.registerModal = false
this.isGuest = false
this.loadWorkspaces()
setTimeout(() => {
this.$refs.editor.saveFormCreate()
}, 500)
}
}
}
</script>

View File

@ -21,18 +21,21 @@
import store from '~/store'
import Form from 'vform'
import {mapState, mapActions} from 'vuex'
import initForm from "../../mixins/form_editor/initForm";
const FormEditor = () => import('../../components/open/forms/components/FormEditor')
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
store.commit('open/templates/stopLoading')
})
}
export default {
name: 'CreateForm',
mixins: [initForm],
components: {
FormEditor,
},
@ -41,7 +44,7 @@ export default {
return {title: 'Create a new Form'}
},
beforeRouteEnter (to, from, next) {
beforeRouteEnter(to, from, next) {
loadTemplates()
next()
},
@ -75,12 +78,6 @@ export default {
workspace() {
return this.$store.getters['open/workspaces/getCurrent']()
},
fromOnboarding() {
return this.$route.params.from_onboarding
},
fbGroupLink() {
return window.config.links.facebook_group
}
},
watch: {
@ -95,15 +92,12 @@ export default {
},
mounted() {
if(this.$route.query.template !== undefined && this.$route.query.template){
let template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
if(template && template.structure){
this.form = new Form(template.structure)
}else{
this.initForm()
this.initForm()
if (this.$route.query.template !== undefined && this.$route.query.template) {
const template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
if (template && template.structure) {
this.form = new Form({...this.form.data(), ...template.structure})
}
}else{
this.initForm()
}
this.closeAlert()
this.loadWorkspaces()
@ -122,50 +116,6 @@ export default {
...mapActions({
loadWorkspaces: 'open/workspaces/loadIfEmpty'
}),
initForm() {
this.form = new Form({
title: 'My Form',
description: null,
visibility: 'public',
workspace_id: this.workspace?.id,
properties: [],
notifies: false,
slack_notifies: false,
send_submission_confirmation: false,
webhook_url: null,
// Customization
theme: 'default',
width: 'centered',
dark_mode: 'auto',
color: '#3B82F6',
hide_title: false,
no_branding: false,
uppercase_labels: true,
transparent_background: false,
closes_at: null,
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
// Submission
submit_button_text: 'Submit',
re_fillable: false,
re_fill_button_text: 'Fill Again',
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
notification_sender: 'OpnForm',
notification_subject: 'We saved your answers',
notification_body: 'Hello there 👋 <br>This is a confirmation that your submission was successfully saved.',
notifications_include_submission: true,
use_captcha: false,
is_rating: false,
rating_max_value: 5,
max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
// Security & Privacy
can_be_indexed: true
})
},
/**
* Compute max height of editor
*/

View File

@ -1,34 +1,38 @@
<template>
<div class="flex flex-col min-h-full mt-6">
<div class="w-full flex-grow md:w-4/5 lg:w-2/3 md:mx-auto md:max-w-4xl px-4">
<div class="flex flex-col min-h-full mt-6">
<div class="w-full flex-grow md:w-4/5 lg:w-2/3 md:mx-auto md:max-w-4xl px-4">
<breadcrumb :path="breadcrumbs" />
<div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto" />
<breadcrumb :path="breadcrumbs"/>
<div v-if="templatesLoading" class="text-center">
<loader class="h-6 w-6 text-nt-blue mx-auto"/>
</div>
<p v-else-if="template === null || !template">
Template does not exist.
</p>
<div v-else>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
{{ template.name }}
</h2>
</div>
<p v-else-if="template === null || !template">
Template does not exist.
</p>
<div v-else>
<div class="flex flex-wrap items-center mt-6 mb-4">
<h2 class="text-nt-blue text-3xl font-bold flex-grow">
{{ template.name }}
</h2>
<div class="mb-10">
<div class="w-full shadow-xl rounded-lg my-5 max-h-72 flex items-center justify-center overflow-hidden">
<img :src="template.image_url" alt="Template cover image" class="w-full object-cover"/>
</div>
<div v-html="template.description"></div>
<div class="mt-5 text-center">
<v-button class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}">
Use this template
</v-button>
</div>
<div class="mb-10">
<img :src="template.image_url" alt="" class="w-full shadow-xl rounded-lg my-5"/>
<div v-html="template.description"></div>
<div class="mt-5 text-center">
<v-button class="mt-4 sm:mt-0" :to="{path:'/forms/create?template='+template.slug}">
Use this template
</v-button>
</div>
<h3 class="text-center text-gray-500">Template Preview</h3>
<open-complete-form ref="open-complete-form" :form="form" :creating="true" class="my-5 p-4 bg-gray-50 rounded-lg"/>
<h3 class="text-center text-gray-500 mt-6 mb-2">Template Preview</h3>
<open-complete-form ref="open-complete-form" :form="form" :creating="true"
class="mb-4 p-4 bg-gray-50 rounded-lg overflow-hidden"/>
<div v-if="template.questions.length > 0" id="questions">
<h3 class="text-xl font-semibold mb-3">Frequently asked questions</h3>
<div v-if="template.questions.length > 0" class="mt-5 pt-2">
<div class="mt-5 pt-2">
<div v-for="(ques,ques_key) in template.questions" :key="ques_key" class="my-3 border rounded-lg">
<h5 class="border-b p-2">{{ ques.question }}</h5>
<div class="p-2" v-html="ques.answer"></div>
@ -37,64 +41,65 @@
</div>
</div>
</div>
<open-form-footer class="mt-8 border-t" />
</div>
</template>
<open-form-footer class="mt-8 border-t"/>
</div>
</template>
<script>
import store from '~/store'
import Form from 'vform'
import { mapGetters, mapState } from 'vuex'
import Fuse from 'fuse.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm'
import Breadcrumb from "../../components/common/Breadcrumb";
<script>
import store from '~/store'
import Form from 'vform'
import {mapGetters, mapState} from 'vuex'
import Fuse from 'fuse.js'
import OpenFormFooter from '../../components/pages/OpenFormFooter'
import OpenCompleteForm from '../../components/open/forms/OpenCompleteForm'
import Breadcrumb from "../../components/common/Breadcrumb";
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
const loadTemplates = function () {
store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading')
})
}
export default {
components: {Breadcrumb, OpenFormFooter, OpenCompleteForm },
export default {
components: {Breadcrumb, OpenFormFooter, OpenCompleteForm},
beforeRouteEnter (to, from, next) {
loadTemplates()
next()
},
beforeRouteEnter(to, from, next) {
loadTemplates()
next()
},
props: {
metaTitle: { type: String, default: 'Templates' },
metaDescription: { type: String, default: 'Public templates for create form quickly!' }
},
props: {
metaTitle: {type: String, default: 'Templates'},
metaDescription: {type: String, default: 'Public templates for create form quickly!'}
},
data () {
return {
data() {
return {}
},
mounted() {
},
methods: {},
computed: {
...mapState({
templatesLoading: state => state['open/templates'].loading
}),
breadcrumbs() {
if (!this.template) {
return [{route: {name: 'templates'}, label: 'Templates'}]
}
return [{route: {name: 'templates'}, label: 'Templates'}, {label: this.template.name}]
},
mounted () {},
methods: {},
computed: {
...mapState({
templatesLoading: state => state['open/templates'].loading
}),
breadcrumbs () {
if (!this.template) {
return [{ route: { name: 'templates' }, label: 'Templates' }]
}
return [{ route: { name: 'templates' }, label: 'Templates' }, { label: this.template.name }]
},
template () {
return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug)
},
form (){
return new Form(this.template.structure)
}
template() {
return this.$store.getters['open/templates/getBySlug'](this.$route.params.slug)
},
form() {
return new Form(this.template.structure)
}
}
</script>
}
</script>

View File

@ -16,7 +16,7 @@
>it's free</span>.
</h3>
<div class="mt-6 flex justify-center">
<v-button class="mr-1" :to="{ name: 'register' }" :arrow="true">
<v-button class="mr-1" :to="{ name: 'forms.create.guest' }" :arrow="true">
Create a form for FREE
</v-button>
</div>
@ -73,7 +73,7 @@
<h4 class="font-semibold text-3xl text-white">Take your forms to the next level</h4>
<p class="text-gray-300 my-8">No trial. Generous, unlimited free plan.</p>
<div class="mt-6 flex justify-center">
<v-button :to="{ name: 'register' }" v-track.welcome_create_form_click :arrow="true" color="blue">
<v-button :to="{ name: 'forms.create.guest' }" v-track.welcome_create_form_click :arrow="true" color="blue">
Create a form for FREE
</v-button>
</div>

View File

@ -8,6 +8,7 @@ export default [
// Forms
{ path: '/forms/create', name: 'forms.create', component: page('forms/create.vue') },
{ path: '/forms/create/guest', name: 'forms.create.guest', component: page('forms/create-guest.vue') },
{ path: '/forms/:slug/edit', name: 'forms.edit', component: page('forms/edit.vue') },
{
path: '/forms/:slug/show',