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-prism-editor": "^1.2.2",
"vue-router": "^3.5.2", "vue-router": "^3.5.2",
"vue-tailwind": "^2.5.0", "vue-tailwind": "^2.5.0",
"vue-tour": "^2.0.0",
"vue2-editor": "^2.10.3", "vue2-editor": "^2.10.3",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.6.2", "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 LoadScript from 'vue-plugin-load-script'
import Base from './base' import Base from './base'
import VueTour from 'vue-tour'
import '~/plugins' import '~/plugins'
import '~/components' import '~/components'
@ -16,10 +14,6 @@ Vue.config.productionTip = false
Vue.mixin(Base) Vue.mixin(Base)
Vue.use(LoadScript) Vue.use(LoadScript)
/* Vue Tour */
require('vue-tour/dist/vue-tour.css')
Vue.use(VueTour)
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
i18n, i18n,

View File

@ -15,10 +15,10 @@
<workspace-dropdown class="ml-6"/> <workspace-dropdown class="ml-6"/>
</div> </div>
<div class="hidden md:block ml-auto relative"> <div class="hidden md:block ml-auto relative">
<!-- <router-link :to="{name:'integrations'}"--> <router-link :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">
<!-- Integrations--> Templates
<!-- </router-link>--> </router-link>
<a href="#" class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1" <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" @click.prevent="$getCrisp().push(['do', 'helpdesk:search'])" v-if="hasCrisp"
> >
@ -99,7 +99,7 @@
{{ $t('login') }} {{ $t('login') }}
</router-link> </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 Create a form
</v-button> </v-button>

View File

@ -7,17 +7,17 @@
</div> </div>
<modal :show="showPremiumModal" @close="showPremiumModal=false"> <modal :show="showPremiumModal" @close="showPremiumModal=false">
<h2 class="text-nt-blue"> <h2 class="text-nt-blue">
OpenForm PRO OpnForm PRO
</h2> </h2>
<h4 v-if="user.is_subscribed && !user.has_enterprise_subscription" class="text-center mt-5"> <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>. feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
<br><br> <br><br>
If you need to collaborate, or to work with multiple workspaces, or just larger file uploads, you can 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. also upgrade our subscription to get an Enterprise subscription.
</h4> </h4>
<h4 v-if="user.is_subscribed && user.has_enterprise_subscription" class="text-center mt-5"> <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>. feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
</h4> </h4>
<p v-if="!user.is_subscribed" class="mt-4"> <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" class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
> >
PRO 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 within
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
to to

View File

@ -91,7 +91,7 @@
> >
<template #submit-btn="{submitForm}"> <template #submit-btn="{submitForm}">
<open-form-button :loading="loading" :theme="theme" :color="form.color" class="mt-2 px-8 mx-1" <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 }} {{ form.submit_button_text }}
</open-form-button> </open-form-button>
@ -193,7 +193,7 @@ export default {
}) })
window.localStorage.removeItem(this.form.form_pending_submission_Key) window.localStorage.removeItem(this.form.form_pending_submission_Key)
if (response.data.redirect && response.data.redirect_url) { if (response.data.redirect && response.data.redirect_url) {
window.location.href = response.data.redirect_url window.location.href = response.data.redirect_url
} }

View File

@ -1,14 +1,13 @@
<template> <template>
<div v-if="form" id="form-editor" class="w-full flex flex-grow relative overflow-x-hidden"> <div v-if="form" id="form-editor" class="w-full flex flex-grow relative overflow-x-hidden">
<!-- Form fields selection --> <!-- 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="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"> <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 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. to preview your form changes.
</div> </div>
<div class="p-4 pb-0"> <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"> <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" <path d="M5 9L1 5L5 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round"/> stroke-linejoin="round"/>
@ -88,6 +87,11 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
isGuest: {
required: false,
type: Boolean,
default: false
},
}, },
data() { data() {
@ -123,7 +127,7 @@ export default {
{ {
target: '#v-step-0', target: '#v-step-0',
header: { header: {
title: 'Welcome to the OpenForm Editor!' title: 'Welcome to the OpnForm Editor!'
}, },
content: 'Discover <strong>your form Editor</strong>!' content: 'Discover <strong>your form Editor</strong>!'
}, },
@ -160,20 +164,16 @@ export default {
mounted() { mounted() {
this.$emit('mounted') this.$emit('mounted')
this.startTour()
}, },
methods: { methods: {
startTour() {
if (!this.user.has_forms) {
this.$tours.tutorial.start()
}
},
showValidationErrors() { showValidationErrors() {
this.showFormErrorModal = true this.showFormErrorModal = true
}, },
saveForm() { saveForm() {
if (this.isEdit) { if(this.isGuest) {
this.saveFormGuest()
} else if (this.isEdit) {
this.saveFormEdit() this.saveFormEdit()
} else { } else {
this.saveFormCreate() this.saveFormCreate()
@ -230,6 +230,9 @@ export default {
}).finally(() => { }).finally(() => {
this.updateFormLoading = false this.updateFormLoading = false
}) })
},
saveFormGuest() {
this.$emit('openRegister')
} }
} }
} }

View File

@ -266,7 +266,7 @@ export default {
]; ];
}, },
init() { 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() this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields()
} else { } else {
this.formFields = clonedeep(this.form.properties).map((field) => { this.formFields = clonedeep(this.form.properties).map((field) => {

View File

@ -47,6 +47,17 @@
</svg> </svg>
Duplicate form Duplicate form
</a> </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="#" <a href="#"
class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center" 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}" v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
@ -60,23 +71,12 @@
</svg> </svg>
Delete form Delete form
</a> </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> </dropdown>
<create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/> <create-template-modal :form="form" :show="showCreateTemplateModal" @close="showCreateTemplateModal=false"/>
</div> </div>
</template> </template>
<script> <script>
import axios from 'axios' import axios from 'axios'
import {mapGetters, mapState} from 'vuex' import {mapGetters, mapState} from 'vuex'
@ -127,4 +127,3 @@ export default {
} }
} }
</script> </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> <template>
<div> <div>
<forgot-password-modal :show="showForgotModal" @close="showForgotModal=false" />
<div class="flex mt-6 mb-10"> <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: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"> <div class="w-full md:w-1/2 md:p-6">
@ -11,35 +9,7 @@
</h2> </h2>
<small>Welcome back! Please enter your details.</small> <small>Welcome back! Please enter your details.</small>
<form @submit.prevent="login" @keydown="form.onKeydown($event)" class="mt-4"> <login-form />
<!-- 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>
</div> </div>
</div> </div>
<div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 "> <div class="w-full md:w-1/2 md:p-6 mt-8 md:mt-0 ">
@ -86,17 +56,15 @@
</template> </template>
<script> <script>
import Form from 'vform'
import Cookies from 'js-cookie'
import OpenFormFooter from '../../components/pages/OpenFormFooter' import OpenFormFooter from '../../components/pages/OpenFormFooter'
import Testimonials from '../../components/pages/welcome/Testimonials' import Testimonials from '../../components/pages/welcome/Testimonials'
import ForgotPasswordModal from './ForgotPasswordModal' import LoginForm from './components/LoginForm'
export default { export default {
components: { components: {
OpenFormFooter, OpenFormFooter,
Testimonials, Testimonials,
ForgotPasswordModal LoginForm
}, },
middleware: 'guest', middleware: 'guest',
@ -106,42 +74,11 @@ export default {
}, },
data: () => ({ data: () => ({
form: new Form({
email: '',
password: ''
}),
remember: false,
showForgotModal: false
}), }),
methods: { 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> </script>

View File

@ -9,43 +9,7 @@
</h2> </h2>
<small>Sign up in less than 2 minutes.</small> <small>Sign up in less than 2 minutes.</small>
<form @submit.prevent="register" @keydown="form.onKeydown($event)" class="mt-4"> <register-form />
<!-- 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>
</div> </div>
</div> </div>
<div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0 "> <div class="w-full hidden lg:block lg:w-1/2 md:p-6 mt-8 md:mt-0 ">
@ -92,19 +56,15 @@
</template> </template>
<script> <script>
import Form from 'vform'
import LoginWithGithub from '~/components/LoginWithGithub'
import SelectInput from '../../components/forms/SelectInput'
import OpenFormFooter from '../../components/pages/OpenFormFooter' import OpenFormFooter from '../../components/pages/OpenFormFooter'
import { initCrisp } from '../../middleware/check-auth'
import Testimonials from '../../components/pages/welcome/Testimonials' import Testimonials from '../../components/pages/welcome/Testimonials'
import RegisterForm from './components/RegisterForm'
export default { export default {
components: { components: {
Testimonials, Testimonials,
SelectInput, OpenFormFooter,
LoginWithGithub, RegisterForm
OpenFormFooter
}, },
middleware: 'guest', middleware: 'guest',
@ -114,62 +74,13 @@ export default {
}, },
data: () => ({ data: () => ({
form: new Form({
name: '',
email: '',
password: '',
password_confirmation: '',
agree_terms: false
}),
mustVerifyEmail: false
}), }),
computed: { 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: { 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> </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 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";
const FormEditor = () => import('../../components/open/forms/components/FormEditor') const FormEditor = () => import('../../components/open/forms/components/FormEditor')
const loadTemplates = function () { const loadTemplates = function () {
store.commit('open/templates/startLoading') store.commit('open/templates/startLoading')
store.dispatch('open/templates/loadIfEmpty').then(() => { store.dispatch('open/templates/loadIfEmpty').then(() => {
store.commit('open/templates/stopLoading') store.commit('open/templates/stopLoading')
}) })
} }
export default { export default {
name: 'CreateForm', name: 'CreateForm',
mixins: [initForm],
components: { components: {
FormEditor, FormEditor,
}, },
@ -41,7 +44,7 @@ export default {
return {title: 'Create a new Form'} return {title: 'Create a new Form'}
}, },
beforeRouteEnter (to, from, next) { beforeRouteEnter(to, from, next) {
loadTemplates() loadTemplates()
next() next()
}, },
@ -75,12 +78,6 @@ export default {
workspace() { workspace() {
return this.$store.getters['open/workspaces/getCurrent']() return this.$store.getters['open/workspaces/getCurrent']()
}, },
fromOnboarding() {
return this.$route.params.from_onboarding
},
fbGroupLink() {
return window.config.links.facebook_group
}
}, },
watch: { watch: {
@ -95,15 +92,12 @@ export default {
}, },
mounted() { mounted() {
if(this.$route.query.template !== undefined && this.$route.query.template){ this.initForm()
let template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template) if (this.$route.query.template !== undefined && this.$route.query.template) {
if(template && template.structure){ const template = this.$store.getters['open/templates/getBySlug'](this.$route.query.template)
this.form = new Form(template.structure) if (template && template.structure) {
}else{ this.form = new Form({...this.form.data(), ...template.structure})
this.initForm()
} }
}else{
this.initForm()
} }
this.closeAlert() this.closeAlert()
this.loadWorkspaces() this.loadWorkspaces()
@ -122,50 +116,6 @@ export default {
...mapActions({ ...mapActions({
loadWorkspaces: 'open/workspaces/loadIfEmpty' 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 * Compute max height of editor
*/ */

View File

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

View File

@ -16,7 +16,7 @@
>it's free</span>. >it's free</span>.
</h3> </h3>
<div class="mt-6 flex justify-center"> <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 Create a form for FREE
</v-button> </v-button>
</div> </div>
@ -73,7 +73,7 @@
<h4 class="font-semibold text-3xl text-white">Take your forms to the next level</h4> <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> <p class="text-gray-300 my-8">No trial. Generous, unlimited free plan.</p>
<div class="mt-6 flex justify-center"> <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 Create a form for FREE
</v-button> </v-button>
</div> </div>

View File

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